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 {}