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