vortex_array/aggregate_fn/
accumulator.rs1use std::any::Any;
5
6use vortex_error::VortexExpect;
7use vortex_error::VortexResult;
8use vortex_error::vortex_bail;
9use vortex_error::vortex_ensure;
10use vortex_error::vortex_ensure_eq;
11
12use crate::ArrayRef;
13use crate::Columnar;
14use crate::ExecutionCtx;
15use crate::aggregate_fn::AggregateDTypes;
16use crate::aggregate_fn::AggregateFn;
17use crate::aggregate_fn::AggregateFnRef;
18use crate::aggregate_fn::AggregateFnVTable;
19use crate::aggregate_fn::session::AggregateFnSessionExt;
20use crate::columnar::AnyColumnar;
21use crate::dtype::DType;
22use crate::executor::max_iterations;
23use crate::expr::stats::Precision;
24use crate::expr::stats::Stat;
25use crate::expr::stats::StatsProvider;
26use crate::scalar::Scalar;
27
28pub type AccumulatorRef = Box<dyn DynAccumulator>;
30
31pub struct Accumulator<V: AggregateFnVTable> {
33 vtable: V,
35 options: V::Options,
37 aggregate_fn: AggregateFnRef,
39 dtypes: AggregateDTypes,
41 partial: Option<V::Partial>,
43}
44
45impl<V: AggregateFnVTable> Accumulator<V> {
46 pub fn try_new(vtable: V, options: V::Options, dtype: DType) -> VortexResult<Self> {
47 let dtypes = AggregateDTypes::try_new(&vtable, &options, dtype)?;
48
49 Ok(Self::from_dtypes(vtable, options, dtypes))
50 }
51
52 pub fn from_dtypes(vtable: V, options: V::Options, dtypes: AggregateDTypes) -> Self {
57 let aggregate_fn = AggregateFn::new(vtable.clone(), options.clone()).erased();
58
59 Self {
60 vtable,
61 options,
62 aggregate_fn,
63 dtypes,
64 partial: None,
65 }
66 }
67
68 pub(crate) fn empty_partial(&self) -> VortexResult<V::Partial> {
70 self.vtable.empty_partial(self.dtypes.args(&self.options))
71 }
72
73 fn ensure_partial(&mut self) -> VortexResult<()> {
74 if self.partial.is_none() {
75 self.partial = Some(self.empty_partial()?);
76 }
77 Ok(())
78 }
79
80 pub(crate) fn fold_partial(&mut self, other: V::Partial) -> VortexResult<()> {
81 self.partial = Some(match self.partial.take() {
82 None => other,
84 Some(current) => {
85 self.vtable
86 .merge_partials(self.dtypes.args(&self.options), current, other)?
87 }
88 });
89 Ok(())
90 }
91
92 fn fold_partial_scalar(&mut self, scalar: &Scalar) -> VortexResult<()> {
93 let other = self
94 .vtable
95 .partial_from_scalar(self.dtypes.args(&self.options), scalar)?;
96 self.fold_partial(other)
97 }
98}
99
100pub trait DynAccumulator: 'static + Send {
103 fn accumulate(&mut self, batch: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()>;
105
106 fn merge_from(&mut self, other: &mut dyn DynAccumulator) -> VortexResult<()>;
111
112 fn combine_partials(&mut self, partial: &Scalar) -> VortexResult<()>;
117
118 fn is_saturated(&self) -> bool;
120
121 fn reset(&mut self);
123
124 fn partial_scalar(&self) -> VortexResult<Scalar>;
128
129 fn final_scalar(&self) -> VortexResult<Scalar>;
131
132 fn flush(&mut self) -> VortexResult<Scalar>;
136
137 fn finish(&mut self) -> VortexResult<Scalar>;
141
142 fn as_any_mut(&mut self) -> &mut dyn Any;
144}
145
146impl dyn DynAccumulator {
147 pub fn downcast_mut<V: AggregateFnVTable>(&mut self) -> Option<&mut Accumulator<V>> {
149 self.as_any_mut().downcast_mut()
150 }
151}
152
153impl<V: AggregateFnVTable> DynAccumulator for Accumulator<V> {
154 fn accumulate(&mut self, batch: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()> {
155 if self.is_saturated() {
156 return Ok(());
157 }
158
159 vortex_ensure_eq!(batch.dtype(), &self.dtypes.dtype, "Input DType mismatch");
160
161 if let Some(stat) = Stat::from_aggregate_fn(&self.aggregate_fn)
164 && let Precision::Exact(partial) = batch.statistics().get(stat)
165 {
166 let partial = if partial.dtype() == &self.dtypes.partial_dtype {
167 partial
168 } else {
169 vortex_ensure!(
170 partial
171 .dtype()
172 .eq_ignore_nullability(&self.dtypes.partial_dtype),
173 "Aggregate {} read legacy stat {} with dtype {}, expected {}",
174 self.aggregate_fn,
175 stat,
176 partial.dtype(),
177 self.dtypes.partial_dtype,
178 );
179 partial.cast(&self.dtypes.partial_dtype)?
180 };
181 self.fold_partial_scalar(&partial)?;
182 return Ok(());
183 }
184
185 let session = ctx.session().clone();
186
187 {
193 let kernel = session
194 .aggregate_fns()
195 .find_aggregate_kernel(batch.encoding_id(), self.aggregate_fn.id());
196 if let Some(kernel) = kernel
197 && let Some(result) = kernel.aggregate(&self.aggregate_fn, batch, ctx)?
198 {
199 vortex_ensure_eq!(
200 result.dtype(),
201 &self.dtypes.partial_dtype,
202 "Aggregate kernel returned the wrong partial dtype",
203 );
204 self.fold_partial_scalar(&result)?;
205 return Ok(());
206 }
207 }
208
209 self.ensure_partial()?;
211 let partial = self.partial.as_mut().vortex_expect("partial materialized");
212 if self
213 .vtable
214 .try_accumulate(self.dtypes.args(&self.options), partial, batch, ctx)?
215 {
216 return Ok(());
217 }
218
219 let mut batch = batch.clone();
225 for _ in 0..max_iterations() {
226 if batch.is::<AnyColumnar>() {
227 break;
228 }
229
230 if let Some(kernel) = session
231 .aggregate_fns()
232 .find_aggregate_kernel(batch.encoding_id(), self.aggregate_fn.id())
233 && let Some(result) = kernel.aggregate(&self.aggregate_fn, &batch, ctx)?
234 {
235 vortex_ensure_eq!(
236 result.dtype(),
237 &self.dtypes.partial_dtype,
238 "Aggregate kernel returned the wrong partial dtype",
239 );
240 self.fold_partial_scalar(&result)?;
241 return Ok(());
242 }
243
244 batch = batch.execute(ctx)?;
245 }
246
247 let columnar = batch.execute::<Columnar>(ctx)?;
249
250 self.ensure_partial()?;
251 let partial = self.partial.as_mut().vortex_expect("partial materialized");
252 self.vtable
253 .accumulate(self.dtypes.args(&self.options), partial, &columnar, ctx)
254 }
255
256 fn merge_from(&mut self, other: &mut dyn DynAccumulator) -> VortexResult<()> {
257 let Some(other) = other.downcast_mut::<V>() else {
258 vortex_bail!(
259 "Cannot merge into a {} accumulator from an accumulator of a different aggregate",
260 self.aggregate_fn,
261 );
262 };
263 vortex_ensure!(
264 other.options == self.options && other.dtypes.dtype == self.dtypes.dtype,
265 "Cannot merge {} accumulators with different options or input dtypes",
266 self.aggregate_fn,
267 );
268 match other.partial.take() {
269 Some(partial) => self.fold_partial(partial),
270 None => Ok(()),
271 }
272 }
273
274 fn combine_partials(&mut self, partial: &Scalar) -> VortexResult<()> {
275 vortex_ensure_eq!(
276 partial.dtype(),
277 &self.dtypes.partial_dtype,
278 "Partial DType mismatch for {}",
279 self.aggregate_fn,
280 );
281 self.fold_partial_scalar(partial)
282 }
283
284 fn as_any_mut(&mut self) -> &mut dyn Any {
285 self
286 }
287
288 fn is_saturated(&self) -> bool {
289 self.partial.as_ref().is_some_and(|partial| {
290 self.vtable
291 .is_saturated(self.dtypes.args(&self.options), partial)
292 })
293 }
294
295 fn reset(&mut self) {
296 self.partial = None;
297 }
298
299 fn partial_scalar(&self) -> VortexResult<Scalar> {
300 let args = self.dtypes.args(&self.options);
301 let partial = match &self.partial {
302 Some(partial) => self.vtable.to_scalar(args, partial)?,
303 None => self.vtable.to_scalar(args, &self.empty_partial()?)?,
304 };
305
306 #[cfg(debug_assertions)]
307 {
308 vortex_ensure_eq!(
309 partial.dtype(),
310 args.partial_dtype,
311 "Aggregate returned incorrect DType on partial_scalar",
312 );
313 }
314
315 Ok(partial)
316 }
317
318 fn final_scalar(&self) -> VortexResult<Scalar> {
319 let args = self.dtypes.args(&self.options);
320 let result = match &self.partial {
321 Some(partial) => self.vtable.finalize_scalar(args, partial)?,
322 None => self.vtable.finalize_scalar(args, &self.empty_partial()?)?,
323 };
324
325 vortex_ensure_eq!(
326 result.dtype(),
327 args.return_dtype,
328 "Aggregate returned incorrect DType on final_scalar",
329 );
330
331 Ok(result)
332 }
333
334 fn flush(&mut self) -> VortexResult<Scalar> {
335 let partial = self.partial_scalar()?;
336 self.reset();
337 Ok(partial)
338 }
339
340 fn finish(&mut self) -> VortexResult<Scalar> {
341 let result = self.final_scalar()?;
342 self.reset();
343 Ok(result)
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use vortex_buffer::buffer;
350 use vortex_error::VortexResult;
351 use vortex_session::SessionExt;
352 use vortex_session::VortexSession;
353
354 use crate::ArrayRef;
355 use crate::ExecutionCtx;
356 use crate::IntoArray;
357 use crate::VortexSessionExecute;
358 use crate::aggregate_fn::Accumulator;
359 use crate::aggregate_fn::AccumulatorRef;
360 use crate::aggregate_fn::AggregateFnRef;
361 use crate::aggregate_fn::AggregateFnVTable;
362 use crate::aggregate_fn::DynAccumulator;
363 use crate::aggregate_fn::NumericalAggregateOpts;
364 use crate::aggregate_fn::combined::Combined;
365 use crate::aggregate_fn::combined::PairOptions;
366 use crate::aggregate_fn::fns::mean::Mean;
367 use crate::aggregate_fn::fns::min::Min;
368 use crate::aggregate_fn::fns::sum::Sum;
369 use crate::aggregate_fn::kernels::DynAggregateKernel;
370 use crate::aggregate_fn::session::AggregateFnSession;
371 use crate::array::VTable;
372 use crate::arrays::Dict;
373 use crate::arrays::DictArray;
374 use crate::dtype::DType;
375 use crate::dtype::Nullability;
376 use crate::dtype::PType;
377 use crate::expr::stats::Precision;
378 use crate::expr::stats::Stat;
379 use crate::scalar::Scalar;
380 use crate::scalar::ScalarValue;
381
382 #[derive(Debug)]
386 struct SentinelMeanPartialKernel;
387 impl DynAggregateKernel for SentinelMeanPartialKernel {
388 fn aggregate(
389 &self,
390 _aggregate_fn: &AggregateFnRef,
391 _batch: &ArrayRef,
392 _ctx: &mut ExecutionCtx,
393 ) -> VortexResult<Option<Scalar>> {
394 Ok(Some(sentinel_partial()))
395 }
396 }
397
398 #[derive(Debug)]
400 struct DeclineKernel;
401 impl DynAggregateKernel for DeclineKernel {
402 fn aggregate(
403 &self,
404 _aggregate_fn: &AggregateFnRef,
405 _batch: &ArrayRef,
406 _ctx: &mut ExecutionCtx,
407 ) -> VortexResult<Option<Scalar>> {
408 Ok(None)
409 }
410 }
411
412 #[derive(Debug)]
414 struct SentinelSumPartialKernel;
415 impl DynAggregateKernel for SentinelSumPartialKernel {
416 fn aggregate(
417 &self,
418 _aggregate_fn: &AggregateFnRef,
419 _batch: &ArrayRef,
420 _ctx: &mut ExecutionCtx,
421 ) -> VortexResult<Option<Scalar>> {
422 Ok(Some(Scalar::primitive(42.0f64, Nullability::Nullable)))
423 }
424 }
425
426 fn fresh_session() -> VortexSession {
427 crate::array_session()
428 }
429
430 fn dict_of_seven() -> ArrayRef {
431 DictArray::try_new(buffer![0u32].into_array(), buffer![7.0f64].into_array())
432 .expect("valid dictionary")
433 .into_array()
434 }
435
436 fn mean_f64_accumulator() -> VortexResult<Accumulator<Combined<Mean>>> {
437 let dtype = DType::Primitive(PType::F64, Nullability::NonNullable);
438 Accumulator::try_new(
439 Mean::combined(),
440 PairOptions(
441 NumericalAggregateOpts::default(),
442 NumericalAggregateOpts::default(),
443 ),
444 dtype,
445 )
446 }
447
448 fn sentinel_partial() -> Scalar {
449 let acc = mean_f64_accumulator().expect("build accumulator");
450 let sum = Scalar::primitive(42.0f64, Nullability::Nullable);
451 let count = Scalar::primitive(1u64, Nullability::NonNullable);
452 Scalar::struct_(acc.dtypes.partial_dtype, vec![sum, count])
453 }
454
455 #[test]
458 fn combined_kernel_fires() -> VortexResult<()> {
459 static KERNEL: SentinelMeanPartialKernel = SentinelMeanPartialKernel;
460 let session = fresh_session();
461 session
462 .get::<AggregateFnSession>()
463 .register_aggregate_kernel(Dict.id(), Some(Mean::combined().id()), &KERNEL);
464 let mut ctx = session.create_execution_ctx();
465
466 let mut acc = mean_f64_accumulator()?;
467 acc.accumulate(&dict_of_seven(), &mut ctx)?;
468 let partial = acc.flush()?;
469
470 let s = partial.as_struct();
471 assert_eq!(
472 s.field("sum").unwrap().as_primitive().as_::<f64>(),
473 Some(42.0)
474 );
475 assert_eq!(
476 s.field("count").unwrap().as_primitive().as_::<u64>(),
477 Some(1)
478 );
479 Ok(())
480 }
481
482 #[test]
485 fn fallback_when_kernel_declines() -> VortexResult<()> {
486 static KERNEL: DeclineKernel = DeclineKernel;
487 let session = fresh_session();
488 session
489 .get::<AggregateFnSession>()
490 .register_aggregate_kernel(Dict.id(), Some(Mean::combined().id()), &KERNEL);
491 let mut ctx = session.create_execution_ctx();
492
493 let mut acc = mean_f64_accumulator()?;
494 acc.accumulate(&dict_of_seven(), &mut ctx)?;
495 let partial = acc.flush()?;
496
497 let s = partial.as_struct();
498 assert_eq!(
499 s.field("sum").unwrap().as_primitive().as_::<f64>(),
500 Some(7.0)
501 );
502 assert_eq!(
503 s.field("count").unwrap().as_primitive().as_::<u64>(),
504 Some(1)
505 );
506 Ok(())
507 }
508
509 #[test]
513 fn child_kernel_fires_through_combined() -> VortexResult<()> {
514 static KERNEL: SentinelSumPartialKernel = SentinelSumPartialKernel;
515 let session = fresh_session();
516 session
517 .get::<AggregateFnSession>()
518 .register_aggregate_kernel(Dict.id(), Some(Sum.id()), &KERNEL);
519 let mut ctx = session.create_execution_ctx();
520
521 let mut acc = mean_f64_accumulator()?;
522 acc.accumulate(&dict_of_seven(), &mut ctx)?;
523 let partial = acc.flush()?;
524
525 let s = partial.as_struct();
526 assert_eq!(
530 s.field("sum").unwrap().as_primitive().as_::<f64>(),
531 Some(42.0)
532 );
533 assert_eq!(
534 s.field("count").unwrap().as_primitive().as_::<u64>(),
535 Some(1)
536 );
537 Ok(())
538 }
539
540 #[test]
541 fn cached_sum_precedes_encoding_kernel() -> VortexResult<()> {
542 static KERNEL: SentinelSumPartialKernel = SentinelSumPartialKernel;
543 let session = fresh_session();
544 session
545 .get::<AggregateFnSession>()
546 .register_aggregate_kernel(Dict.id(), Some(Sum.id()), &KERNEL);
547 let mut ctx = session.create_execution_ctx();
548
549 let batch = dict_of_seven();
550 batch
551 .statistics()
552 .set(Stat::Sum, Precision::Exact(ScalarValue::from(11.0f64)));
553
554 let dtype = DType::Primitive(PType::F64, Nullability::NonNullable);
555 let mut acc = Accumulator::try_new(Sum, NumericalAggregateOpts::default(), dtype)?;
556 acc.accumulate(&batch, &mut ctx)?;
557
558 assert_eq!(acc.finish()?.as_primitive().as_::<f64>(), Some(11.0));
559 Ok(())
560 }
561
562 fn sum_i32_accumulator(options: NumericalAggregateOpts) -> VortexResult<AccumulatorRef> {
563 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
564 Ok(Box::new(Accumulator::try_new(Sum, options, dtype)?))
565 }
566
567 #[test]
568 fn merge_from_drains_other_accumulator() -> VortexResult<()> {
569 let mut ctx = fresh_session().create_execution_ctx();
570 let mut global = sum_i32_accumulator(NumericalAggregateOpts::default())?;
571 let mut local = sum_i32_accumulator(NumericalAggregateOpts::default())?;
572
573 global.accumulate(&buffer![10i32, 20].into_array(), &mut ctx)?;
574 local.accumulate(&buffer![5i32].into_array(), &mut ctx)?;
575 global.merge_from(local.as_mut())?;
576
577 assert_eq!(
578 global.finish()?,
579 Scalar::primitive(35i64, Nullability::Nullable)
580 );
581 assert_eq!(
583 local.finish()?,
584 Scalar::primitive(0i64, Nullability::Nullable)
585 );
586 Ok(())
587 }
588
589 #[test]
590 fn merge_from_rejects_a_different_aggregate() -> VortexResult<()> {
591 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
592 let mut sum = sum_i32_accumulator(NumericalAggregateOpts::default())?;
593 let mut min: AccumulatorRef = Box::new(Accumulator::try_new(
594 Min,
595 NumericalAggregateOpts::default(),
596 dtype,
597 )?);
598
599 assert!(sum.merge_from(min.as_mut()).is_err());
600 Ok(())
601 }
602
603 #[test]
604 fn merge_from_rejects_mismatched_options() -> VortexResult<()> {
605 let mut skipping = sum_i32_accumulator(NumericalAggregateOpts::skip_nans())?;
606 let mut including = sum_i32_accumulator(NumericalAggregateOpts::include_nans())?;
607
608 assert!(skipping.merge_from(including.as_mut()).is_err());
609 Ok(())
610 }
611
612 #[test]
613 fn merge_from_combines_child_accumulators() -> VortexResult<()> {
614 let mut ctx = fresh_session().create_execution_ctx();
615 let mut global: AccumulatorRef = Box::new(mean_f64_accumulator()?);
616 let mut local: AccumulatorRef = Box::new(mean_f64_accumulator()?);
617
618 global.accumulate(&buffer![1.0f64, 2.0].into_array(), &mut ctx)?;
619 local.accumulate(&buffer![6.0f64].into_array(), &mut ctx)?;
620 global.merge_from(local.as_mut())?;
621
622 assert_eq!(global.finish()?.as_primitive().as_::<f64>(), Some(3.0));
623 Ok(())
624 }
625}