Skip to main content

vortex_array/aggregate_fn/
vtable.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt;
5use std::fmt::Debug;
6use std::fmt::Display;
7use std::fmt::Formatter;
8use std::hash::Hash;
9
10use prost::Message;
11use vortex_error::VortexResult;
12use vortex_error::vortex_bail;
13use vortex_proto::expr as pb;
14use vortex_session::VortexSession;
15
16use crate::ArrayRef;
17use crate::Columnar;
18use crate::ExecutionCtx;
19use crate::aggregate_fn::AggregateFn;
20use crate::aggregate_fn::AggregateFnId;
21use crate::aggregate_fn::AggregateFnRef;
22use crate::aggregate_fn::AggregateFnSatisfaction;
23use crate::dtype::DType;
24use crate::scalar::Scalar;
25
26/// Defines the interface for aggregate function vtables.
27///
28/// This trait is non-object-safe and allows the implementer to make use of associated types
29/// for improved type safety, while allowing Vortex to enforce runtime checks on the inputs and
30/// outputs of each function.
31///
32/// The [`AggregateFnVTable`] trait should be implemented for a struct that holds global data across
33/// all instances of the aggregate. In almost all cases, this struct will be an empty unit
34/// struct, since most aggregates do not require any global state.
35pub trait AggregateFnVTable: 'static + Sized + Clone + Send + Sync {
36    /// Options for this aggregate function.
37    type Options: 'static + Send + Sync + Clone + Debug + Display + PartialEq + Eq + Hash;
38
39    /// The partial accumulator state for a single group.
40    type Partial: 'static + Send;
41
42    /// Returns the ID of the aggregate function vtable.
43    fn id(&self) -> AggregateFnId;
44
45    /// Serialize the options for this aggregate function.
46    ///
47    /// Should return `Ok(None)` if the function is not serializable, and `Ok(vec![])` if it is
48    /// serializable but has no metadata.
49    fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
50        _ = options;
51        Ok(None)
52    }
53
54    /// Deserialize the options of this aggregate function.
55    fn deserialize(
56        &self,
57        _metadata: &[u8],
58        _session: &VortexSession,
59    ) -> VortexResult<Self::Options> {
60        vortex_bail!("Aggregate function {} is not deserializable", self.id());
61    }
62
63    /// Coerce the input type for this aggregate function.
64    ///
65    /// This is optionally used by Vortex users when performing type coercion over a Vortex
66    /// expression. The default implementation returns the input type unchanged.
67    fn coerce_args(&self, options: &Self::Options, input_dtype: &DType) -> VortexResult<DType> {
68        let _ = options;
69        Ok(input_dtype.clone())
70    }
71
72    /// Return whether this stored aggregate can satisfy `requested`.
73    ///
74    /// The default implementation only treats exactly equal aggregate functions as satisfying the
75    /// request. Approximate pruning aggregates can override this to expose looser-but-sound bounds.
76    fn can_satisfy(
77        &self,
78        options: &Self::Options,
79        requested: &AggregateFnRef,
80    ) -> AggregateFnSatisfaction {
81        if requested
82            .as_opt::<Self>()
83            .is_some_and(|other| other == options)
84        {
85            AggregateFnSatisfaction::Exact
86        } else {
87            AggregateFnSatisfaction::No
88        }
89    }
90
91    /// The return [`DType`] of the aggregate.
92    ///
93    /// Returns `None` if the aggregate function cannot be applied to the input dtype.
94    fn return_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType>;
95
96    /// If this aggregate should be computed as a default zone statistic for `input_dtype`, return
97    /// the bound aggregate to store. Default: not a zone-map default.
98    fn zone_stat_default(&self, _input_dtype: &DType) -> Option<AggregateFnRef> {
99        None
100    }
101
102    /// DType of the intermediate partial accumulator state.
103    ///
104    /// Use a struct dtype when multiple fields are needed
105    /// (e.g., Mean: `Struct { sum: f64, count: u64 }`).
106    ///
107    /// Returns `None` if the aggregate function cannot be applied to the input dtype.
108    fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType>;
109
110    /// Return the partial accumulator state for an empty group.
111    fn empty_partial(
112        &self,
113        options: &Self::Options,
114        input_dtype: &DType,
115    ) -> VortexResult<Self::Partial>;
116
117    /// Combine partial scalar state into the accumulator.
118    fn combine_partials(&self, partial: &mut Self::Partial, other: Scalar) -> VortexResult<()>;
119
120    /// Convert the partial state into a partial scalar.
121    ///
122    /// The returned scalar must have the same DType as specified by `partial_dtype` for the
123    /// options and input dtype used to construct the state.
124    fn to_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar>;
125
126    /// Reset the state of the accumulator to an empty group.
127    fn reset(&self, partial: &mut Self::Partial);
128
129    /// Is the partial accumulator state is "saturated", i.e. has it reached a state where the
130    /// final result is fully determined.
131    fn is_saturated(&self, state: &Self::Partial) -> bool;
132
133    /// Try to accumulate the raw array before decompression.
134    ///
135    /// Returns `true` if the array was handled, `false` to fall through to
136    /// the default kernel dispatch and canonicalization path.
137    ///
138    /// This is useful for aggregates that only depend on array metadata (e.g., validity)
139    /// rather than the encoded data, avoiding unnecessary decompression.
140    fn try_accumulate(
141        &self,
142        _state: &mut Self::Partial,
143        _batch: &ArrayRef,
144        _ctx: &mut ExecutionCtx,
145    ) -> VortexResult<bool> {
146        Ok(false)
147    }
148
149    /// Accumulate a new canonical array into the accumulator state.
150    fn accumulate(
151        &self,
152        state: &mut Self::Partial,
153        batch: &Columnar,
154        ctx: &mut ExecutionCtx,
155    ) -> VortexResult<()>;
156
157    /// Finalize an array of accumulator states into an array of aggregate results.
158    ///
159    /// The provides `states` array has dtype as specified by `state_dtype`, the result array
160    /// must have dtype as specified by `return_dtype`.
161    fn finalize(&self, states: ArrayRef) -> VortexResult<ArrayRef>;
162
163    /// Finalize a scalar accumulator state into an aggregate result.
164    ///
165    /// The provided `state` has dtype as specified by `state_dtype`, the result scalar must have
166    /// dtype as specified by `return_dtype`.
167    fn finalize_scalar(&self, partial: &Self::Partial) -> VortexResult<Scalar>;
168}
169
170#[derive(Clone, Debug, PartialEq, Eq, Hash)]
171pub struct EmptyOptions;
172impl Display for EmptyOptions {
173    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
174        write!(f, "")
175    }
176}
177
178/// Options for aggregate functions over primitive numeric inputs, controlling how NaN values in
179/// floating-point arrays are handled.
180///
181/// When `skip_nans` is `true` (the default), NaN values are treated as missing: they contribute
182/// nothing to `sum`/`min`/`max`/`mean` and are excluded from `count`.
183///
184/// When `skip_nans` is `false`, NaN values participate in the aggregate: `count` includes them,
185/// while any NaN value poisons the result of `sum`/`min`/`max`/`mean` to NaN.
186///
187/// The option has no effect on non-float inputs.
188#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
189pub struct NumericalAggregateOpts {
190    /// Whether NaN values are skipped (treated as missing) during aggregation.
191    pub skip_nans: bool,
192}
193
194impl NumericalAggregateOpts {
195    /// Options that skip NaN values, treating them as missing during aggregation.
196    ///
197    /// This is the default configuration; see [`NumericalAggregateOpts::include_nans`] for the
198    /// NaN-including variant.
199    pub const fn skip_nans() -> Self {
200        Self { skip_nans: true }
201    }
202
203    /// Options that include NaN values in the aggregate: `count` counts them, while any NaN
204    /// poisons the result of `sum`/`min`/`max`/`mean` to NaN.
205    ///
206    /// See [`NumericalAggregateOpts::skip_nans`] for the default NaN-skipping variant.
207    pub const fn include_nans() -> Self {
208        Self { skip_nans: false }
209    }
210
211    /// Serialize these options to protobuf-encoded metadata bytes.
212    pub fn serialize(&self) -> Vec<u8> {
213        pb::NumericalAggregateOpts {
214            skip_nans: self.skip_nans,
215        }
216        .encode_to_vec()
217    }
218
219    /// Deserialize these options from protobuf-encoded metadata bytes.
220    pub fn deserialize(metadata: &[u8]) -> VortexResult<Self> {
221        let opts = pb::NumericalAggregateOpts::decode(metadata)?;
222        Ok(Self {
223            skip_nans: opts.skip_nans,
224        })
225    }
226}
227
228impl Default for NumericalAggregateOpts {
229    fn default() -> Self {
230        Self::skip_nans()
231    }
232}
233
234impl Display for NumericalAggregateOpts {
235    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
236        // Only the non-default configuration is displayed, so that aggregates with default
237        // options render identically to their pre-options form, e.g. `vortex.sum()`.
238        if !self.skip_nans {
239            write!(f, "skip_nans=false")?;
240        }
241        Ok(())
242    }
243}
244
245/// Factory functions for aggregate vtables.
246pub trait AggregateFnVTableExt: AggregateFnVTable {
247    /// Bind this vtable with the given options into an [`AggregateFnRef`].
248    fn bind(&self, options: Self::Options) -> AggregateFnRef {
249        AggregateFn::new(self.clone(), options).erased()
250    }
251}
252impl<V: AggregateFnVTable> AggregateFnVTableExt for V {}