vortex_array/aggregate_fn/fns/max/
mod.rs1use vortex_error::VortexExpect;
5use vortex_error::VortexResult;
6use vortex_session::VortexSession;
7use vortex_session::registry::CachedId;
8
9use crate::ArrayRef;
10use crate::Columnar;
11use crate::ExecutionCtx;
12use crate::aggregate_fn::AggregateArgs;
13use crate::aggregate_fn::AggregateFnId;
14use crate::aggregate_fn::AggregateFnRef;
15use crate::aggregate_fn::AggregateFnSatisfaction;
16use crate::aggregate_fn::AggregateFnVTable;
17use crate::aggregate_fn::NumericalAggregateOpts;
18use crate::aggregate_fn::fns::bounded_max::BoundedMax;
19use crate::aggregate_fn::fns::min_max::MinMax;
20use crate::aggregate_fn::fns::min_max::columnar_min_max;
21use crate::aggregate_fn::fns::min_max::nan_scalar;
22use crate::aggregate_fn::fns::min_max::scalar_is_nan;
23use crate::dtype::DType;
24use crate::expr::stats::Precision;
25use crate::expr::stats::Stat;
26use crate::expr::stats::StatsProvider;
27use crate::expr::stats::StatsProviderExt;
28use crate::partial_ord::partial_max;
29use crate::scalar::Scalar;
30
31#[derive(Clone, Debug)]
36pub struct Max;
37
38pub struct MaxPartial {
40 max: Option<Scalar>,
41}
42
43impl MaxPartial {
44 fn merge(&mut self, args: AggregateArgs<'_, NumericalAggregateOpts>, max: Scalar) {
45 if max.is_null() {
46 return;
47 }
48
49 if scalar_is_nan(&max) || self.is_poisoned() {
52 if !args.options.skip_nans {
53 self.poison(args);
54 }
55 return;
56 }
57
58 self.max = Some(match self.max.take() {
59 Some(current) => partial_max(max, current).vortex_expect("incomparable max scalars"),
60 None => max,
61 });
62 }
63
64 fn poison(&mut self, args: AggregateArgs<'_, NumericalAggregateOpts>) {
65 self.max = Some(nan_scalar(args.dtype));
66 }
67
68 fn is_poisoned(&self) -> bool {
69 self.max.as_ref().is_some_and(scalar_is_nan)
70 }
71}
72
73impl AggregateFnVTable for Max {
74 type Options = NumericalAggregateOpts;
75 type Partial = MaxPartial;
76
77 fn id(&self) -> AggregateFnId {
78 static ID: CachedId = CachedId::new("vortex.max");
79 *ID
80 }
81
82 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
83 Ok(Some(options.serialize()))
84 }
85
86 fn deserialize(
87 &self,
88 metadata: &[u8],
89 _session: &VortexSession,
90 ) -> VortexResult<Self::Options> {
91 NumericalAggregateOpts::deserialize(metadata)
92 }
93
94 fn return_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
95 MinMax
96 .return_dtype(options, input_dtype)
97 .map(|_| input_dtype.as_nullable())
98 }
99
100 fn can_satisfy(
101 &self,
102 options: &Self::Options,
103 requested: &AggregateFnRef,
104 ) -> AggregateFnSatisfaction {
105 if requested
106 .as_opt::<Self>()
107 .is_some_and(|other| other == options)
108 {
109 AggregateFnSatisfaction::Exact
110 } else if requested.is::<BoundedMax>() && options.skip_nans {
111 AggregateFnSatisfaction::Approximate
113 } else {
114 AggregateFnSatisfaction::No
115 }
116 }
117
118 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
119 self.return_dtype(options, input_dtype)
120 }
121
122 fn empty_partial(
123 &self,
124 _args: AggregateArgs<'_, Self::Options>,
125 ) -> VortexResult<Self::Partial> {
126 Ok(MaxPartial { max: None })
127 }
128
129 fn partial_from_scalar(
130 &self,
131 args: AggregateArgs<'_, Self::Options>,
132 scalar: &Scalar,
133 ) -> VortexResult<Self::Partial> {
134 let mut partial = MaxPartial { max: None };
135 partial.merge(args, scalar.clone());
137 Ok(partial)
138 }
139
140 fn merge_partials(
141 &self,
142 args: AggregateArgs<'_, Self::Options>,
143 mut first: Self::Partial,
144 second: Self::Partial,
145 ) -> VortexResult<Self::Partial> {
146 if let Some(max) = second.max {
147 first.merge(args, max);
148 }
149 Ok(first)
150 }
151
152 fn to_scalar(
153 &self,
154 args: AggregateArgs<'_, Self::Options>,
155 partial: &Self::Partial,
156 ) -> VortexResult<Scalar> {
157 let dtype = args.dtype.as_nullable();
158 match &partial.max {
159 Some(max) => max.cast(&dtype),
160 None => Ok(Scalar::null(dtype)),
161 }
162 }
163
164 fn is_saturated(
165 &self,
166 _args: AggregateArgs<'_, Self::Options>,
167 partial: &Self::Partial,
168 ) -> bool {
169 partial.is_poisoned()
171 }
172
173 fn try_accumulate(
174 &self,
175 args: AggregateArgs<'_, Self::Options>,
176 partial: &mut Self::Partial,
177 batch: &ArrayRef,
178 _ctx: &mut ExecutionCtx,
179 ) -> VortexResult<bool> {
180 if args.options.skip_nans || !args.dtype.is_float() {
183 return Ok(false);
184 }
185 match batch.statistics().get_as::<u64>(Stat::NaNCount) {
186 Precision::Exact(0) => {
187 if let Some(max) = batch.statistics().get(Stat::Max).as_exact() {
190 partial.merge(args, max);
191 return Ok(true);
192 }
193 Ok(false)
194 }
195 Precision::Exact(_) => {
196 partial.poison(args);
197 Ok(true)
198 }
199 _ => Ok(false),
200 }
201 }
202
203 fn accumulate(
204 &self,
205 args: AggregateArgs<'_, Self::Options>,
206 partial: &mut Self::Partial,
207 batch: &Columnar,
208 ctx: &mut ExecutionCtx,
209 ) -> VortexResult<()> {
210 if let Some(result) = columnar_min_max(batch, *args.options, ctx)? {
213 partial.merge(args, result.max);
214 }
215 Ok(())
216 }
217
218 fn finalize(
219 &self,
220 _args: AggregateArgs<'_, Self::Options>,
221 partials: ArrayRef,
222 ) -> VortexResult<ArrayRef> {
223 Ok(partials)
224 }
225
226 fn finalize_scalar(
227 &self,
228 args: AggregateArgs<'_, Self::Options>,
229 partial: &Self::Partial,
230 ) -> VortexResult<Scalar> {
231 self.to_scalar(args, partial)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use vortex_buffer::buffer;
238 use vortex_error::VortexResult;
239
240 use crate::IntoArray as _;
241 use crate::VortexSessionExecute;
242 use crate::aggregate_fn::Accumulator;
243 use crate::aggregate_fn::DynAccumulator;
244 use crate::aggregate_fn::NumericalAggregateOpts;
245 use crate::aggregate_fn::fns::max::Max;
246 use crate::array_session;
247 use crate::arrays::PrimitiveArray;
248 use crate::dtype::DType;
249 use crate::dtype::Nullability;
250 use crate::dtype::PType;
251 use crate::expr::stats::Precision;
252 use crate::expr::stats::Stat;
253 use crate::scalar::Scalar;
254 use crate::scalar::ScalarValue;
255 use crate::validity::Validity;
256
257 #[test]
258 fn max_aggregate_fn() -> VortexResult<()> {
259 let mut ctx = array_session().create_execution_ctx();
260 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
261 let mut acc = Accumulator::try_new(Max, NumericalAggregateOpts::default(), dtype)?;
262
263 let batch1 = PrimitiveArray::new(buffer![10i32, 20, 5], Validity::NonNullable).into_array();
264 acc.accumulate(&batch1, &mut ctx)?;
265
266 let batch2 = PrimitiveArray::new(buffer![3i32, 25], Validity::NonNullable).into_array();
267 acc.accumulate(&batch2, &mut ctx)?;
268
269 assert_eq!(
270 acc.finish()?,
271 Scalar::primitive(25i32, Nullability::Nullable)
272 );
273 Ok(())
274 }
275
276 #[test]
277 fn max_empty_group_returns_null() -> VortexResult<()> {
278 let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
279 let mut acc = Accumulator::try_new(Max, NumericalAggregateOpts::default(), dtype)?;
280
281 assert_eq!(
282 acc.finish()?,
283 Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable))
284 );
285 Ok(())
286 }
287
288 #[test]
289 fn max_with_nan_not_skipping() -> VortexResult<()> {
290 let mut ctx = array_session().create_execution_ctx();
291 let dtype = DType::Primitive(PType::F64, Nullability::NonNullable);
292 let mut acc = Accumulator::try_new(Max, NumericalAggregateOpts::include_nans(), dtype)?;
293
294 let batch = PrimitiveArray::new(buffer![1.0f64, f64::NAN, -5.0], Validity::NonNullable)
295 .into_array();
296 acc.accumulate(&batch, &mut ctx)?;
297 assert!(acc.is_saturated());
298
299 let result = acc.finish()?;
300 assert!(
301 result
302 .as_primitive()
303 .typed_value::<f64>()
304 .is_some_and(f64::is_nan)
305 );
306 Ok(())
307 }
308
309 #[test]
310 fn max_not_skipping_with_exact_nan_count_stat() -> VortexResult<()> {
311 let mut ctx = array_session().create_execution_ctx();
312 let batch =
314 PrimitiveArray::new(buffer![1.0f64, 2.0, f64::NAN], Validity::NonNullable).into_array();
315 batch
316 .statistics()
317 .set(Stat::NaNCount, Precision::Exact(ScalarValue::from(1u64)));
318 let mut acc = Accumulator::try_new(
319 Max,
320 NumericalAggregateOpts::include_nans(),
321 batch.dtype().clone(),
322 )?;
323 acc.accumulate(&batch, &mut ctx)?;
324 let result = acc.finish()?;
325 assert!(
326 result
327 .as_primitive()
328 .typed_value::<f64>()
329 .is_some_and(f64::is_nan)
330 );
331 Ok(())
332 }
333
334 #[test]
335 fn max_nan_including_nullable_cached_stat() -> VortexResult<()> {
336 let mut ctx = array_session().create_execution_ctx();
339 let array =
340 PrimitiveArray::from_option_iter([Some(1.0f64), Some(2.0), Some(3.0)]).into_array();
341 array
342 .statistics()
343 .set(Stat::NaNCount, Precision::Exact(ScalarValue::from(0u64)));
344 array
345 .statistics()
346 .set(Stat::Max, Precision::Exact(ScalarValue::from(3.0f64)));
347 let mut acc = Accumulator::try_new(
348 Max,
349 NumericalAggregateOpts::include_nans(),
350 array.dtype().clone(),
351 )?;
352 acc.accumulate(&array, &mut ctx)?;
353 assert_eq!(
354 acc.finish()?,
355 Scalar::primitive(3.0f64, Nullability::Nullable)
356 );
357 Ok(())
358 }
359
360 #[test]
361 fn max_casts_nonnullable_legacy_stat_to_nullable_partial() -> VortexResult<()> {
362 let mut ctx = array_session().create_execution_ctx();
363 let batch = PrimitiveArray::new(buffer![10i32, 20], Validity::NonNullable).into_array();
364 batch
365 .statistics()
366 .set(Stat::Max, Precision::Exact(ScalarValue::from(20i32)));
367 let mut acc = Accumulator::try_new(
368 Max,
369 NumericalAggregateOpts::default(),
370 batch.dtype().clone(),
371 )?;
372
373 acc.accumulate(&batch, &mut ctx)?;
374
375 assert_eq!(
376 acc.finish()?,
377 Scalar::primitive(20i32, Nullability::Nullable)
378 );
379 Ok(())
380 }
381}