1use std::mem;
9
10use vortex_error::VortexResult;
11use vortex_error::vortex_bail;
12use vortex_error::vortex_err;
13use vortex_session::VortexSession;
14
15use crate::ArrayRef;
16use crate::Canonical;
17use crate::Columnar;
18use crate::ExecutionCtx;
19use crate::aggregate_fn::Accumulator;
20use crate::aggregate_fn::AggregateArgs;
21use crate::aggregate_fn::AggregateDTypes;
22use crate::aggregate_fn::AggregateFnId;
23use crate::aggregate_fn::AggregateFnVTable;
24use crate::aggregate_fn::DynAccumulator;
25use crate::arrays::ConstantArray;
26use crate::arrays::Extension;
27use crate::arrays::extension::ExtensionArrayExt;
28use crate::dtype::DType;
29use crate::dtype::extension::ExtVTable;
30use crate::scalar::Scalar;
31
32#[derive(Clone, Debug)]
111pub struct ExtensionStoragePushdown<A: AggregateFnVTable, E: ExtVTable> {
112 aggregate: A,
113 ext: E,
114 id: AggregateFnId,
115}
116
117impl<A: AggregateFnVTable, E: ExtVTable> ExtensionStoragePushdown<A, E> {
118 pub fn new(aggregate: A, ext: E) -> Self {
123 #[expect(clippy::disallowed_methods, reason = "interning a dynamic id")]
124 let id = AggregateFnId::new(&format!("{}[{}]", aggregate.id(), ext.id()));
125 Self { aggregate, ext, id }
126 }
127
128 fn storage_dtype<'a>(&self, dtype: &'a DType) -> Option<&'a DType> {
130 dtype
131 .as_extension_opt()
132 .filter(|ext_dtype| ext_dtype.is::<E>())
133 .map(|ext_dtype| ext_dtype.storage_dtype())
134 }
135
136 fn storage_args<'a>(
141 &self,
142 args: AggregateArgs<'a, A::Options>,
143 ) -> VortexResult<AggregateArgs<'a, A::Options>> {
144 let dtype = self.storage_dtype(args.dtype).ok_or_else(|| {
145 vortex_err!(
146 "Aggregate function {} expects extension type {}, got {}",
147 self.id,
148 self.ext.id(),
149 args.dtype
150 )
151 })?;
152 Ok(AggregateArgs { dtype, ..args })
153 }
154
155 fn accumulate_storage(
158 &self,
159 args: AggregateArgs<'_, A::Options>,
160 state: &mut A::Partial,
161 storage: &ArrayRef,
162 ctx: &mut ExecutionCtx,
163 ) -> VortexResult<()> {
164 let mut accumulator = Accumulator::from_dtypes(
165 self.aggregate.clone(),
166 args.options.clone(),
167 AggregateDTypes::new(
168 args.dtype.clone(),
169 args.return_dtype.clone(),
170 args.partial_dtype.clone(),
171 ),
172 );
173 accumulator.accumulate(storage, ctx)?;
174 let Some(batch) = accumulator.take_partial() else {
175 return Ok(());
176 };
177 let running = mem::replace(state, self.aggregate.empty_partial(args)?);
178 *state = self.aggregate.merge_partials(args, running, batch)?;
179 Ok(())
180 }
181}
182
183impl<A: AggregateFnVTable, E: ExtVTable> AggregateFnVTable for ExtensionStoragePushdown<A, E> {
184 type Options = A::Options;
185 type Partial = A::Partial;
186
187 fn id(&self) -> AggregateFnId {
188 self.id
189 }
190
191 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
192 self.aggregate.serialize(options)
193 }
194
195 fn deserialize(&self, metadata: &[u8], session: &VortexSession) -> VortexResult<Self::Options> {
196 self.aggregate.deserialize(metadata, session)
197 }
198
199 fn return_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
200 self.aggregate
201 .return_dtype(options, self.storage_dtype(input_dtype)?)
202 }
203
204 fn partial_dtype(&self, options: &Self::Options, input_dtype: &DType) -> Option<DType> {
205 self.aggregate
206 .partial_dtype(options, self.storage_dtype(input_dtype)?)
207 }
208
209 fn empty_partial(&self, args: AggregateArgs<'_, Self::Options>) -> VortexResult<Self::Partial> {
210 self.aggregate.empty_partial(self.storage_args(args)?)
211 }
212
213 fn partial_from_scalar(
214 &self,
215 args: AggregateArgs<'_, Self::Options>,
216 scalar: &Scalar,
217 ) -> VortexResult<Self::Partial> {
218 self.aggregate
219 .partial_from_scalar(self.storage_args(args)?, scalar)
220 }
221
222 fn merge_partials(
223 &self,
224 args: AggregateArgs<'_, Self::Options>,
225 first: Self::Partial,
226 second: Self::Partial,
227 ) -> VortexResult<Self::Partial> {
228 self.aggregate
229 .merge_partials(self.storage_args(args)?, first, second)
230 }
231
232 fn to_scalar(
233 &self,
234 args: AggregateArgs<'_, Self::Options>,
235 partial: &Self::Partial,
236 ) -> VortexResult<Scalar> {
237 self.aggregate.to_scalar(self.storage_args(args)?, partial)
238 }
239
240 fn is_saturated(
241 &self,
242 args: AggregateArgs<'_, Self::Options>,
243 partial: &Self::Partial,
244 ) -> bool {
245 self.storage_args(args)
246 .is_ok_and(|args| self.aggregate.is_saturated(args, partial))
247 }
248
249 fn try_accumulate(
252 &self,
253 args: AggregateArgs<'_, Self::Options>,
254 state: &mut Self::Partial,
255 batch: &ArrayRef,
256 ctx: &mut ExecutionCtx,
257 ) -> VortexResult<bool> {
258 let Some(array) = batch.as_opt::<Extension>() else {
259 return Ok(false);
260 };
261 self.accumulate_storage(self.storage_args(args)?, state, array.storage_array(), ctx)?;
262 Ok(true)
263 }
264
265 fn accumulate(
266 &self,
267 args: AggregateArgs<'_, Self::Options>,
268 state: &mut Self::Partial,
269 batch: &Columnar,
270 ctx: &mut ExecutionCtx,
271 ) -> VortexResult<()> {
272 let args = self.storage_args(args)?;
273 match batch {
274 Columnar::Canonical(Canonical::Extension(array)) => {
275 self.accumulate_storage(args, state, array.storage_array(), ctx)
276 }
277 Columnar::Constant(constant) => {
280 let storage = constant.scalar().as_extension().to_storage_scalar();
281 self.aggregate.accumulate(
282 args,
283 state,
284 &Columnar::Constant(ConstantArray::new(storage, constant.len())),
285 ctx,
286 )
287 }
288 Columnar::Canonical(_) => {
289 vortex_bail!(
290 "Aggregate function {} expects an extension array, got {}",
291 self.id,
292 batch.dtype()
293 )
294 }
295 }
296 }
297
298 fn finalize(
299 &self,
300 args: AggregateArgs<'_, Self::Options>,
301 states: ArrayRef,
302 ) -> VortexResult<ArrayRef> {
303 self.aggregate.finalize(self.storage_args(args)?, states)
304 }
305
306 fn finalize_scalar(
307 &self,
308 args: AggregateArgs<'_, Self::Options>,
309 partial: &Self::Partial,
310 ) -> VortexResult<Scalar> {
311 self.aggregate
312 .finalize_scalar(self.storage_args(args)?, partial)
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use rstest::rstest;
319 use vortex_error::VortexResult;
320
321 use super::ExtensionStoragePushdown;
322 use crate::IntoArray;
323 use crate::VortexSessionExecute;
324 use crate::aggregate_fn::Accumulator;
325 use crate::aggregate_fn::AggregateFnVTable;
326 use crate::aggregate_fn::DynAccumulator;
327 use crate::aggregate_fn::NumericalAggregateOpts;
328 use crate::aggregate_fn::fns::is_sorted::IsSorted;
329 use crate::aggregate_fn::fns::is_sorted::IsSortedOptions;
330 use crate::aggregate_fn::fns::min_max::MinMax;
331 use crate::aggregate_fn::session::AggregateFnSessionExt;
332 use crate::arrays::ConstantArray;
333 use crate::arrays::ExtensionArray;
334 use crate::arrays::PrimitiveArray;
335 use crate::dtype::DType;
336 use crate::dtype::Nullability;
337 use crate::extension::datetime::Date;
338 use crate::extension::datetime::Time;
339 use crate::extension::datetime::TimeUnit;
340 use crate::extension::datetime::Timestamp;
341 use crate::scalar::Scalar;
342
343 #[test]
344 fn pushes_down_into_storage() -> VortexResult<()> {
345 let session = crate::array_session();
346 let aggregate = ExtensionStoragePushdown::new(MinMax, Timestamp);
347 session.aggregate_fns().register(aggregate.clone());
348 assert!(
349 session
350 .aggregate_fns()
351 .find_plugin(&aggregate.id())
352 .is_some()
353 );
354
355 let ext_dtype = Timestamp::new(TimeUnit::Milliseconds, Nullability::Nullable).erased();
356 let storage =
357 PrimitiveArray::from_option_iter([Some(3_000i64), None, Some(1_000)]).into_array();
358 let array = ExtensionArray::new(ext_dtype.clone(), storage.clone()).into_array();
359
360 let mut ctx = session.create_execution_ctx();
361 let mut acc = Accumulator::try_new(
362 aggregate,
363 NumericalAggregateOpts::default(),
364 DType::Extension(ext_dtype),
365 )?;
366 acc.accumulate(&array, &mut ctx)?;
367
368 let mut expected = Accumulator::try_new(
369 MinMax,
370 NumericalAggregateOpts::default(),
371 storage.dtype().clone(),
372 )?;
373 expected.accumulate(&storage, &mut ctx)?;
374
375 assert_eq!(acc.finish()?, expected.finish()?);
376 Ok(())
377 }
378
379 #[test]
380 fn combines_flushed_partials() -> VortexResult<()> {
381 let session = crate::array_session();
382 let mut ctx = session.create_execution_ctx();
383 let ext_dtype = Timestamp::new(TimeUnit::Milliseconds, Nullability::Nullable).erased();
384 let batch = |values: [Option<i64>; 2]| {
385 ExtensionArray::new(
386 ext_dtype.clone(),
387 PrimitiveArray::from_option_iter(values).into_array(),
388 )
389 .into_array()
390 };
391 let accumulator = || {
392 Accumulator::try_new(
393 ExtensionStoragePushdown::new(MinMax, Timestamp),
394 NumericalAggregateOpts::default(),
395 DType::Extension(ext_dtype.clone()),
396 )
397 };
398
399 let mut first = accumulator()?;
400 first.accumulate(&batch([Some(3_000), None]), &mut ctx)?;
401 let mut second = accumulator()?;
402 second.accumulate(&batch([Some(1_000), Some(500)]), &mut ctx)?;
403 first.combine_partials(&second.flush()?)?;
404
405 let mut expected = accumulator()?;
406 expected.accumulate(&batch([Some(3_000), None]), &mut ctx)?;
407 expected.accumulate(&batch([Some(1_000), Some(500)]), &mut ctx)?;
408
409 assert_eq!(first.finish()?, expected.finish()?);
410 Ok(())
411 }
412
413 #[test]
414 fn accumulates_constant_batches() -> VortexResult<()> {
415 let session = crate::array_session();
416 let mut ctx = session.create_execution_ctx();
417 let ext_dtype = Timestamp::new(TimeUnit::Milliseconds, Nullability::Nullable).erased();
418 let constant = |value: i64| {
419 ConstantArray::new(
420 Scalar::extension_ref(
421 ext_dtype.clone(),
422 Scalar::primitive(value, Nullability::Nullable),
423 ),
424 3,
425 )
426 .into_array()
427 };
428
429 let mut acc = Accumulator::try_new(
430 ExtensionStoragePushdown::new(MinMax, Timestamp),
431 NumericalAggregateOpts::default(),
432 DType::Extension(ext_dtype.clone()),
433 )?;
434 acc.accumulate(&constant(2_000), &mut ctx)?;
435 acc.accumulate(&constant(1_000), &mut ctx)?;
436
437 let mut expected = Accumulator::try_new(
438 MinMax,
439 NumericalAggregateOpts::default(),
440 ext_dtype.storage_dtype().clone(),
441 )?;
442 expected.accumulate(
443 &PrimitiveArray::from_option_iter([Some(2_000i64), Some(1_000)]).into_array(),
444 &mut ctx,
445 )?;
446
447 assert_eq!(acc.finish()?, expected.finish()?);
448 Ok(())
449 }
450
451 #[test]
452 fn rejects_other_extension_types() {
453 let date = DType::Extension(Date::new(TimeUnit::Days, Nullability::Nullable).erased());
454 assert!(
455 ExtensionStoragePushdown::new(MinMax, Timestamp)
456 .return_dtype(&NumericalAggregateOpts::default(), &date)
457 .is_none()
458 );
459 }
460
461 #[test]
462 fn default_session_registers_temporal_order_aggregates() {
463 let session = crate::array_session();
464 let aggregate_fns = session.aggregate_fns();
465 for id in [
466 ExtensionStoragePushdown::new(IsSorted, Date).id(),
467 ExtensionStoragePushdown::new(IsSorted, Time).id(),
468 ExtensionStoragePushdown::new(IsSorted, Timestamp).id(),
469 ExtensionStoragePushdown::new(MinMax, Date).id(),
470 ExtensionStoragePushdown::new(MinMax, Time).id(),
471 ExtensionStoragePushdown::new(MinMax, Timestamp).id(),
472 ] {
473 assert!(
474 aggregate_fns.find_plugin(&id).is_some(),
475 "{id} is not registered"
476 );
477 }
478 }
479
480 #[rstest]
481 #[case::sorted([None, Some(1_000), Some(2_000), Some(2_000)], false, true)]
483 #[case::unsorted([Some(2_000), Some(1_000), None, Some(3_000)], false, false)]
484 #[case::strict_with_duplicates([None, Some(1_000), Some(2_000), Some(2_000)], true, false)]
485 fn is_sorted_over_timestamps(
486 #[case] values: [Option<i64>; 4],
487 #[case] strict: bool,
488 #[case] expected: bool,
489 ) -> VortexResult<()> {
490 let session = crate::array_session();
491 let mut ctx = session.create_execution_ctx();
492 let ext_dtype = Timestamp::new(TimeUnit::Milliseconds, Nullability::Nullable).erased();
493 let array = ExtensionArray::new(
494 ext_dtype.clone(),
495 PrimitiveArray::from_option_iter(values).into_array(),
496 )
497 .into_array();
498
499 let mut acc = Accumulator::try_new(
500 ExtensionStoragePushdown::new(IsSorted, Timestamp),
501 IsSortedOptions { strict },
502 DType::Extension(ext_dtype),
503 )?;
504 acc.accumulate(&array, &mut ctx)?;
505
506 assert_eq!(bool::try_from(&acc.finish()?)?, expected);
507 Ok(())
508 }
509
510 #[test]
511 fn min_max_over_dates_matches_storage() -> VortexResult<()> {
512 let session = crate::array_session();
513 let mut ctx = session.create_execution_ctx();
514 let ext_dtype = Date::new(TimeUnit::Days, Nullability::Nullable).erased();
515 let storage =
516 PrimitiveArray::from_option_iter([Some(20i32), None, Some(-3), Some(7)]).into_array();
517 let array = ExtensionArray::new(ext_dtype.clone(), storage.clone()).into_array();
518
519 let mut acc = Accumulator::try_new(
520 ExtensionStoragePushdown::new(MinMax, Date),
521 NumericalAggregateOpts::default(),
522 DType::Extension(ext_dtype),
523 )?;
524 acc.accumulate(&array, &mut ctx)?;
525
526 let mut expected = Accumulator::try_new(
527 MinMax,
528 NumericalAggregateOpts::default(),
529 storage.dtype().clone(),
530 )?;
531 expected.accumulate(&storage, &mut ctx)?;
532
533 assert_eq!(acc.finish()?, expected.finish()?);
534 Ok(())
535 }
536}