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_error::vortex_err;
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::proto::expr as pb;
25use crate::scalar::Scalar;
26
27/// Resolved dtypes of one aggregate function bound to its options and input.
28///
29/// Accumulators resolve these once and pair them with the options as [`AggregateArgs`] for every
30/// execution method, so partial states only hold accumulated values.
31#[derive(Clone, Debug)]
32pub struct AggregateDTypes {
33    /// The DType of the input.
34    pub dtype: DType,
35    /// The DType of the aggregate, as reported by [`AggregateFnVTable::return_dtype`].
36    pub return_dtype: DType,
37    /// The DType of the partial accumulator state, as reported by
38    /// [`AggregateFnVTable::partial_dtype`].
39    pub partial_dtype: DType,
40}
41
42impl AggregateDTypes {
43    /// Pair already-resolved dtypes, e.g. the dtypes a nested aggregate derives for its children.
44    pub fn new(dtype: DType, return_dtype: DType, partial_dtype: DType) -> Self {
45        Self {
46            dtype,
47            return_dtype,
48            partial_dtype,
49        }
50    }
51
52    /// Resolve the return and partial dtypes of `vtable` bound to `options` over `dtype`.
53    ///
54    /// Fails if the aggregate cannot be applied to `dtype`.
55    pub fn try_new<V: AggregateFnVTable>(
56        vtable: &V,
57        options: &V::Options,
58        dtype: DType,
59    ) -> VortexResult<Self> {
60        let return_dtype = vtable.return_dtype(options, &dtype).ok_or_else(|| {
61            vortex_err!(
62                "Aggregate function {} cannot be applied to dtype {}",
63                vtable.id(),
64                dtype
65            )
66        })?;
67        let partial_dtype = vtable.partial_dtype(options, &dtype).ok_or_else(|| {
68            vortex_err!(
69                "Aggregate function {} cannot be applied to dtype {}",
70                vtable.id(),
71                dtype
72            )
73        })?;
74        Ok(Self::new(dtype, return_dtype, partial_dtype))
75    }
76
77    /// Pair these dtypes with `options` for one aggregate execution call.
78    pub fn args<'a, O>(&'a self, options: &'a O) -> AggregateArgs<'a, O> {
79        AggregateArgs {
80            options,
81            dtype: &self.dtype,
82            return_dtype: &self.return_dtype,
83            partial_dtype: &self.partial_dtype,
84        }
85    }
86}
87
88/// The options and resolved dtypes passed to every aggregate execution method.
89#[derive(Debug)]
90pub struct AggregateArgs<'a, O> {
91    /// The options the aggregate function is bound to.
92    pub options: &'a O,
93    /// The DType of the input.
94    pub dtype: &'a DType,
95    /// The DType of the aggregate, as reported by [`AggregateFnVTable::return_dtype`].
96    pub return_dtype: &'a DType,
97    /// The DType of the partial accumulator state, as reported by
98    /// [`AggregateFnVTable::partial_dtype`].
99    pub partial_dtype: &'a DType,
100}
101
102// Derived impls would bound `O: Clone + Copy`, which the borrowed fields do not need.
103impl<O> Clone for AggregateArgs<'_, O> {
104    fn clone(&self) -> Self {
105        *self
106    }
107}
108
109impl<O> Copy for AggregateArgs<'_, O> {}
110
111/// Defines the interface for aggregate function vtables.
112///
113/// This trait is non-object-safe and allows the implementer to make use of associated types
114/// for improved type safety, while allowing Vortex to enforce runtime checks on the inputs and
115/// outputs of each function.
116///
117/// The [`AggregateFnVTable`] trait should be implemented for a struct that holds global data across
118/// all instances of the aggregate. In almost all cases, this struct will be an empty unit
119/// struct, since most aggregates do not require any global state.
120pub trait AggregateFnVTable: 'static + Sized + Clone + Send + Sync {
121    /// Options for this aggregate function.
122    type Options: 'static + Send + Sync + Clone + Debug + Display + PartialEq + Eq + Hash;
123
124    /// The partial accumulator state for a single group.
125    type Partial: 'static + Send;
126
127    /// Returns the ID of the aggregate function vtable.
128    fn id(&self) -> AggregateFnId;
129
130    /// Serialize the options for this aggregate function.
131    ///
132    /// Should return `Ok(None)` if the function is not serializable, and `Ok(vec![])` if it is
133    /// serializable but has no metadata.
134    fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
135        _ = options;
136        Ok(None)
137    }
138
139    /// Deserialize the options of this aggregate function.
140    fn deserialize(
141        &self,
142        _metadata: &[u8],
143        _session: &VortexSession,
144    ) -> VortexResult<Self::Options> {
145        vortex_bail!("Aggregate function {} is not deserializable", self.id());
146    }
147
148    /// Return whether this stored aggregate can satisfy `requested`.
149    ///
150    /// The default implementation only treats exactly equal aggregate functions as satisfying the
151    /// request. Approximate pruning aggregates can override this to expose looser-but-sound bounds.
152    fn can_satisfy(
153        &self,
154        options: &Self::Options,
155        requested: &AggregateFnRef,
156    ) -> AggregateFnSatisfaction {
157        if requested
158            .as_opt::<Self>()
159            .is_some_and(|other| other == options)
160        {
161            AggregateFnSatisfaction::Exact
162        } else {
163            AggregateFnSatisfaction::No
164        }
165    }
166
167    /// The return [`DType`] of the aggregate.
168    ///
169    /// Returns `None` if the aggregate function cannot be applied to the input dtype.
170    fn return_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType>;
171
172    /// If this aggregate should be computed as a default zone statistic for `input_dtype`, return
173    /// the bound aggregate to store. Default: not a zone-map default.
174    fn zone_stat_default(&self, _input_dtype: &DType) -> Option<AggregateFnRef> {
175        None
176    }
177
178    /// DType of the intermediate partial accumulator state.
179    ///
180    /// Use a struct dtype when multiple fields are needed
181    /// (e.g., Mean: `Struct { sum: f64, count: u64 }`).
182    ///
183    /// Returns `None` if the aggregate function cannot be applied to the input dtype.
184    fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType>;
185
186    /// The partial state of a group with no accumulated values.
187    ///
188    /// The identity of [`merge_partials`].
189    ///
190    /// [`merge_partials`]: AggregateFnVTable::merge_partials
191    fn empty_partial(&self, args: AggregateArgs<'_, Self::Options>) -> VortexResult<Self::Partial>;
192
193    /// Parse a partial scalar into the typed partial state.
194    ///
195    /// The scalar must have dtype `args.partial_dtype`; this is the inverse of [`to_scalar`].
196    ///
197    /// [`to_scalar`]: AggregateFnVTable::to_scalar
198    fn partial_from_scalar(
199        &self,
200        args: AggregateArgs<'_, Self::Options>,
201        scalar: Scalar,
202    ) -> VortexResult<Self::Partial>;
203
204    /// Merge two partial states into one.
205    ///
206    /// `first` accumulated the input preceding `second`'s. Order-dependent aggregates (e.g.
207    /// first/last or is_sorted) rely on this, so the merge need not be commutative. Merging with
208    /// [`empty_partial`] on either side is the identity.
209    ///
210    /// [`empty_partial`]: AggregateFnVTable::empty_partial
211    fn merge_partials(
212        &self,
213        args: AggregateArgs<'_, Self::Options>,
214        first: Self::Partial,
215        second: Self::Partial,
216    ) -> VortexResult<Self::Partial>;
217
218    /// Convert the partial state into a partial scalar of dtype `args.partial_dtype`.
219    ///
220    /// This is the inverse of [`partial_from_scalar`]: parsing the returned scalar must
221    /// reconstruct an equivalent state.
222    ///
223    /// [`partial_from_scalar`]: AggregateFnVTable::partial_from_scalar
224    fn to_scalar(
225        &self,
226        args: AggregateArgs<'_, Self::Options>,
227        partial: &Self::Partial,
228    ) -> VortexResult<Scalar>;
229
230    /// Is the partial state "saturated", i.e. has it reached a state where the final result is
231    /// fully determined.
232    fn is_saturated(&self, args: AggregateArgs<'_, Self::Options>, partial: &Self::Partial)
233    -> bool;
234
235    /// Try to accumulate the raw array before decompression.
236    ///
237    /// Returns `true` if the array was handled, `false` to fall through to the default kernel
238    /// dispatch and canonicalization path. When returning `false`, the partial state must be
239    /// unchanged, since the same batch is then accumulated by the fallback path.
240    ///
241    /// This is useful for aggregates that only depend on array metadata (e.g., validity)
242    /// rather than the encoded data, avoiding unnecessary decompression.
243    fn try_accumulate(
244        &self,
245        _args: AggregateArgs<'_, Self::Options>,
246        _state: &mut Self::Partial,
247        _batch: &ArrayRef,
248        _ctx: &mut ExecutionCtx,
249    ) -> VortexResult<bool> {
250        Ok(false)
251    }
252
253    /// Accumulate a new canonical array into the partial state.
254    fn accumulate(
255        &self,
256        args: AggregateArgs<'_, Self::Options>,
257        state: &mut Self::Partial,
258        batch: &Columnar,
259        ctx: &mut ExecutionCtx,
260    ) -> VortexResult<()>;
261
262    /// Finalize an array of partial states into an array of aggregate results.
263    ///
264    /// The `states` array has dtype `args.partial_dtype`; the result must have dtype
265    /// `args.return_dtype`.
266    fn finalize(
267        &self,
268        args: AggregateArgs<'_, Self::Options>,
269        states: ArrayRef,
270    ) -> VortexResult<ArrayRef>;
271
272    /// Finalize a partial state into an aggregate result of dtype `args.return_dtype`.
273    fn finalize_scalar(
274        &self,
275        args: AggregateArgs<'_, Self::Options>,
276        partial: &Self::Partial,
277    ) -> VortexResult<Scalar>;
278}
279
280#[derive(Clone, Debug, PartialEq, Eq, Hash)]
281pub struct EmptyOptions;
282impl Display for EmptyOptions {
283    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
284        write!(f, "")
285    }
286}
287
288/// Options for aggregate functions over primitive numeric inputs, controlling how NaN values in
289/// floating-point arrays are handled.
290///
291/// When `skip_nans` is `true` (the default), NaN values are treated as missing: they contribute
292/// nothing to `sum`/`min`/`max`/`mean` and are excluded from `count`.
293///
294/// When `skip_nans` is `false`, NaN values participate in the aggregate: `count` includes them,
295/// while any NaN value poisons the result of `sum`/`min`/`max`/`mean` to NaN.
296///
297/// The option has no effect on non-float inputs.
298#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
299pub struct NumericalAggregateOpts {
300    /// Whether NaN values are skipped (treated as missing) during aggregation.
301    pub skip_nans: bool,
302}
303
304impl NumericalAggregateOpts {
305    /// Options that skip NaN values, treating them as missing during aggregation.
306    ///
307    /// This is the default configuration; see [`NumericalAggregateOpts::include_nans`] for the
308    /// NaN-including variant.
309    pub const fn skip_nans() -> Self {
310        Self { skip_nans: true }
311    }
312
313    /// Options that include NaN values in the aggregate: `count` counts them, while any NaN
314    /// poisons the result of `sum`/`min`/`max`/`mean` to NaN.
315    ///
316    /// See [`NumericalAggregateOpts::skip_nans`] for the default NaN-skipping variant.
317    pub const fn include_nans() -> Self {
318        Self { skip_nans: false }
319    }
320
321    /// Serialize these options to protobuf-encoded metadata bytes.
322    pub fn serialize(&self) -> Vec<u8> {
323        pb::NumericalAggregateOpts {
324            skip_nans: self.skip_nans,
325        }
326        .encode_to_vec()
327    }
328
329    /// Deserialize these options from protobuf-encoded metadata bytes.
330    pub fn deserialize(metadata: &[u8]) -> VortexResult<Self> {
331        let opts = pb::NumericalAggregateOpts::decode(metadata)?;
332        Ok(Self {
333            skip_nans: opts.skip_nans,
334        })
335    }
336}
337
338impl Default for NumericalAggregateOpts {
339    fn default() -> Self {
340        Self::skip_nans()
341    }
342}
343
344impl Display for NumericalAggregateOpts {
345    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
346        // Only the non-default configuration is displayed, so that aggregates with default
347        // options render identically to their pre-options form, e.g. `vortex.sum()`.
348        if !self.skip_nans {
349            write!(f, "skip_nans=false")?;
350        }
351        Ok(())
352    }
353}
354
355/// Factory functions for aggregate vtables.
356pub trait AggregateFnVTableExt: AggregateFnVTable {
357    /// Bind this vtable with the given options into an [`AggregateFnRef`].
358    fn bind(&self, options: Self::Options) -> AggregateFnRef {
359        AggregateFn::new(self.clone(), options).erased()
360    }
361}
362impl<V: AggregateFnVTable> AggregateFnVTableExt for V {}