1use std::future;
5use std::marker::PhantomData;
6use std::sync::Arc;
7
8use futures::StreamExt;
9use itertools::Itertools;
10use parking_lot::Mutex;
11use vortex_array::ArrayRef;
12use vortex_array::ExecutionCtx;
13use vortex_array::VortexSessionExecute;
14use vortex_array::aggregate_fn::fns::sum::sum;
15use vortex_array::arrays::StructArray;
16use vortex_array::arrays::struct_::StructArrayExt;
17use vortex_array::builders::ArrayBuilder;
18use vortex_array::builders::BoolBuilder;
19use vortex_array::builders::builder_with_capacity_in;
20use vortex_array::dtype::DType;
21use vortex_array::dtype::FieldName;
22use vortex_array::dtype::Nullability;
23use vortex_array::dtype::PType;
24use vortex_array::expr::stats::Precision;
25use vortex_array::expr::stats::Stat;
26use vortex_array::scalar::Scalar;
27use vortex_array::scalar::ScalarTruncation;
28use vortex_array::scalar::lower_bound;
29use vortex_array::scalar::upper_bound;
30use vortex_array::stats::StatsSet;
31use vortex_array::validity::Validity;
32use vortex_buffer::BufferString;
33use vortex_buffer::ByteBuffer;
34use vortex_error::VortexExpect;
35use vortex_error::VortexResult;
36use vortex_error::vortex_panic;
37use vortex_session::VortexSession;
38
39use crate::layouts::zoned::MAX_IS_TRUNCATED;
40use crate::layouts::zoned::MIN_IS_TRUNCATED;
41use crate::sequence::SendableSequentialStream;
42use crate::sequence::SequenceId;
43use crate::sequence::SequentialStreamAdapter;
44use crate::sequence::SequentialStreamExt;
45
46pub fn accumulate_stats(
47 stream: SendableSequentialStream,
48 stats: Arc<[Stat]>,
49 max_variable_length_statistics_size: usize,
50 session: &VortexSession,
51) -> (FileStatsAccumulator, SendableSequentialStream) {
52 let accumulator = FileStatsAccumulator::new(
53 stream.dtype(),
54 stats,
55 max_variable_length_statistics_size,
56 session,
57 );
58 let stream = SequentialStreamAdapter::new(
59 stream.dtype().clone(),
60 stream.scan(accumulator.clone(), |acc, item| {
61 future::ready(Some(acc.process(item)))
62 }),
63 )
64 .sendable();
65 (accumulator, stream)
66}
67
68struct StatsAccumulator {
70 builders: Vec<Box<dyn StatsArrayBuilder>>,
71 length: usize,
72}
73
74impl StatsAccumulator {
75 fn new(dtype: &DType, stats: &[Stat], max_variable_length_statistics_size: usize) -> Self {
76 if !supports_file_stats(dtype) {
77 return Self {
78 builders: Vec::new(),
79 length: 0,
80 };
81 }
82
83 let builders = stats
84 .iter()
85 .filter_map(|&stat| {
86 stat.dtype(dtype).map(|stat_dtype| {
87 stats_builder_with_capacity(
88 stat,
89 &stat_dtype.as_nullable(),
90 1024,
91 max_variable_length_statistics_size,
92 )
93 })
94 })
95 .collect::<Vec<_>>();
96
97 Self {
98 builders,
99 length: 0,
100 }
101 }
102
103 fn push_chunk(&mut self, array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()> {
104 for builder in &mut self.builders {
105 if let Some(value) = array.statistics().compute_stat(builder.stat(), ctx)? {
106 builder.append_scalar(value.cast(&value.dtype().as_nullable())?)?;
107 } else {
108 builder.append_null();
109 }
110 }
111 self.length += 1;
112 Ok(())
113 }
114
115 fn as_array(&mut self, ctx: &mut ExecutionCtx) -> VortexResult<Option<StructArray>> {
116 let mut names = Vec::new();
117 let mut fields = Vec::new();
118
119 for builder in self
120 .builders
121 .iter_mut()
122 .sorted_unstable_by_key(|builder| builder.stat())
124 {
125 let values = builder.finish();
126
127 if values.all_invalid(ctx)? {
129 continue;
130 }
131
132 names.extend(values.names);
133 fields.extend(values.arrays);
134 }
135
136 if names.is_empty() {
137 return Ok(None);
138 }
139
140 StructArray::try_new(names.into(), fields, self.length, Validity::NonNullable).map(Some)
141 }
142
143 fn as_stats_set(&mut self, stats: &[Stat], ctx: &mut ExecutionCtx) -> VortexResult<StatsSet> {
145 let mut stats_set = StatsSet::default();
146 let Some(stats_table) = self.as_array(ctx)? else {
147 return Ok(stats_set);
148 };
149
150 for &stat in stats {
151 let Some(values) = stats_table.unmasked_field_by_name_opt(stat.name()) else {
152 continue;
153 };
154
155 match stat {
156 Stat::Max if is_varlen_dtype(values.dtype()) && !values.all_valid(ctx)? => {
157 continue;
160 }
161 Stat::Sum if !values.all_valid(ctx)? => {
162 continue;
167 }
168 Stat::Min | Stat::Max | Stat::Sum => {
169 if let Some(s) = values.statistics().compute_stat(stat, ctx)?
170 && let Some(v) = s.into_value()
171 {
172 let precision = if stat_was_truncated(&stats_table, stat, ctx)? {
173 Precision::inexact(v)
174 } else {
175 Precision::exact(v)
176 };
177 stats_set.set(stat, precision)
178 }
179 }
180 Stat::NullCount | Stat::NaNCount | Stat::UncompressedSizeInBytes => {
181 if let Some(sum_value) = sum(values, ctx)?
182 .cast(&DType::Primitive(PType::U64, Nullability::Nullable))?
183 .into_value()
184 {
185 stats_set.set(stat, Precision::exact(sum_value));
186 }
187 }
188 Stat::IsConstant | Stat::IsSorted | Stat::IsStrictSorted => {}
189 }
190 }
191 Ok(stats_set)
192 }
193}
194
195fn stat_was_truncated(
196 stats_table: &StructArray,
197 stat: Stat,
198 ctx: &mut ExecutionCtx,
199) -> VortexResult<bool> {
200 let field_name = match stat {
201 Stat::Min => MIN_IS_TRUNCATED,
202 Stat::Max => MAX_IS_TRUNCATED,
203 _ => return Ok(false),
204 };
205 let Some(is_truncated) = stats_table.unmasked_field_by_name_opt(field_name) else {
206 return Ok(false);
207 };
208
209 Ok(is_truncated
210 .statistics()
211 .compute_stat(Stat::Max, ctx)?
212 .is_some_and(|max| max.as_bool().value() == Some(true)))
213}
214
215fn supports_file_stats(dtype: &DType) -> bool {
216 !matches!(dtype, DType::Variant(_))
217}
218
219fn is_varlen_dtype(dtype: &DType) -> bool {
220 matches!(dtype, DType::Utf8(_) | DType::Binary(_))
221}
222
223fn stats_builder_with_capacity(
224 stat: Stat,
225 dtype: &DType,
226 capacity: usize,
227 max_length: usize,
228) -> Box<dyn StatsArrayBuilder> {
229 let values_builder = builder_with_capacity_in(
230 dtype,
231 capacity,
232 vortex_buffer::BufferAllocatorRef::static_ref(),
233 );
234 match stat {
235 Stat::Max => match dtype {
236 DType::Utf8(_) => Box::new(TruncatedMaxBinaryStatsBuilder::<BufferString>::new(
237 values_builder,
238 BoolBuilder::with_capacity_in(
239 Nullability::NonNullable,
240 capacity,
241 vortex_buffer::BufferAllocatorRef::static_ref(),
242 ),
243 max_length,
244 )),
245 DType::Binary(_) => Box::new(TruncatedMaxBinaryStatsBuilder::<ByteBuffer>::new(
246 values_builder,
247 BoolBuilder::with_capacity_in(
248 Nullability::NonNullable,
249 capacity,
250 vortex_buffer::BufferAllocatorRef::static_ref(),
251 ),
252 max_length,
253 )),
254 _ => Box::new(StatNameArrayBuilder::new(stat, values_builder)),
255 },
256 Stat::Min => match dtype {
257 DType::Utf8(_) => Box::new(TruncatedMinBinaryStatsBuilder::<BufferString>::new(
258 values_builder,
259 BoolBuilder::with_capacity_in(
260 Nullability::NonNullable,
261 capacity,
262 vortex_buffer::BufferAllocatorRef::static_ref(),
263 ),
264 max_length,
265 )),
266 DType::Binary(_) => Box::new(TruncatedMinBinaryStatsBuilder::<ByteBuffer>::new(
267 values_builder,
268 BoolBuilder::with_capacity_in(
269 Nullability::NonNullable,
270 capacity,
271 vortex_buffer::BufferAllocatorRef::static_ref(),
272 ),
273 max_length,
274 )),
275 _ => Box::new(StatNameArrayBuilder::new(stat, values_builder)),
276 },
277 _ => Box::new(StatNameArrayBuilder::new(stat, values_builder)),
278 }
279}
280
281struct NamedArrays {
283 names: Vec<FieldName>,
284 arrays: Vec<ArrayRef>,
285}
286
287impl NamedArrays {
288 fn all_invalid(&self, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
289 self.arrays[0].all_invalid(ctx)
290 }
291}
292
293trait StatsArrayBuilder: Send {
294 fn stat(&self) -> Stat;
295
296 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()>;
297
298 fn append_null(&mut self);
299
300 fn finish(&mut self) -> NamedArrays;
301}
302
303struct StatNameArrayBuilder {
304 stat: Stat,
305 builder: Box<dyn ArrayBuilder>,
306}
307
308impl StatNameArrayBuilder {
309 fn new(stat: Stat, builder: Box<dyn ArrayBuilder>) -> Self {
310 Self { stat, builder }
311 }
312}
313
314impl StatsArrayBuilder for StatNameArrayBuilder {
315 fn stat(&self) -> Stat {
316 self.stat
317 }
318
319 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()> {
320 self.builder.append_scalar(&value)
321 }
322
323 fn append_null(&mut self) {
324 self.builder.append_null()
325 }
326
327 fn finish(&mut self) -> NamedArrays {
328 NamedArrays {
329 names: vec![self.stat.name().into()],
330 arrays: vec![self.builder.finish()],
331 }
332 }
333}
334
335struct TruncatedMaxBinaryStatsBuilder<T: ScalarTruncation> {
336 values: Box<dyn ArrayBuilder>,
337 is_truncated: BoolBuilder,
338 max_value_length: usize,
339 _marker: PhantomData<T>,
340}
341
342impl<T: ScalarTruncation> TruncatedMaxBinaryStatsBuilder<T> {
343 fn new(
344 values: Box<dyn ArrayBuilder>,
345 is_truncated: BoolBuilder,
346 max_value_length: usize,
347 ) -> Self {
348 Self {
349 values,
350 is_truncated,
351 max_value_length,
352 _marker: PhantomData,
353 }
354 }
355}
356
357struct TruncatedMinBinaryStatsBuilder<T: ScalarTruncation> {
358 values: Box<dyn ArrayBuilder>,
359 is_truncated: BoolBuilder,
360 max_value_length: usize,
361 _marker: PhantomData<T>,
362}
363
364impl<T: ScalarTruncation> TruncatedMinBinaryStatsBuilder<T> {
365 fn new(
366 values: Box<dyn ArrayBuilder>,
367 is_truncated: BoolBuilder,
368 max_value_length: usize,
369 ) -> Self {
370 Self {
371 values,
372 is_truncated,
373 max_value_length,
374 _marker: PhantomData,
375 }
376 }
377}
378
379impl<T: ScalarTruncation> StatsArrayBuilder for TruncatedMaxBinaryStatsBuilder<T> {
380 fn stat(&self) -> Stat {
381 Stat::Max
382 }
383
384 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()> {
385 let nullability = value.dtype().nullability();
386 if let Some((upper_bound, truncated)) =
387 upper_bound(T::from_scalar(value)?, self.max_value_length, nullability)
388 {
389 self.values.append_scalar(&upper_bound)?;
390 self.is_truncated.append_value(truncated);
391 } else {
392 self.append_null()
393 }
394 Ok(())
395 }
396
397 fn append_null(&mut self) {
398 ArrayBuilder::append_null(self.values.as_mut());
399 self.is_truncated.append_value(false);
400 }
401
402 fn finish(&mut self) -> NamedArrays {
403 NamedArrays {
404 names: vec![Stat::Max.name().into(), MAX_IS_TRUNCATED.into()],
405 arrays: vec![
406 ArrayBuilder::finish(self.values.as_mut()),
407 ArrayBuilder::finish(&mut self.is_truncated),
408 ],
409 }
410 }
411}
412
413impl<T: ScalarTruncation> StatsArrayBuilder for TruncatedMinBinaryStatsBuilder<T> {
414 fn stat(&self) -> Stat {
415 Stat::Min
416 }
417
418 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()> {
419 let nullability = value.dtype().nullability();
420 if let Some((lower_bound, truncated)) =
421 lower_bound(T::from_scalar(value)?, self.max_value_length, nullability)
422 {
423 self.values.append_scalar(&lower_bound)?;
424 self.is_truncated.append_value(truncated);
425 } else {
426 self.append_null()
427 }
428 Ok(())
429 }
430
431 fn append_null(&mut self) {
432 ArrayBuilder::append_null(self.values.as_mut());
433 self.is_truncated.append_value(false);
434 }
435
436 fn finish(&mut self) -> NamedArrays {
437 NamedArrays {
438 names: vec![Stat::Min.name().into(), MIN_IS_TRUNCATED.into()],
439 arrays: vec![
440 ArrayBuilder::finish(self.values.as_mut()),
441 ArrayBuilder::finish(&mut self.is_truncated),
442 ],
443 }
444 }
445}
446
447#[derive(Clone)]
451pub struct FileStatsAccumulator {
452 stats: Arc<[Stat]>,
453 accumulators: Arc<Mutex<Vec<StatsAccumulator>>>,
454 ctx: Arc<Mutex<ExecutionCtx>>,
455}
456
457impl FileStatsAccumulator {
458 fn new(
459 dtype: &DType,
460 stats: Arc<[Stat]>,
461 max_variable_length_statistics_size: usize,
462 session: &VortexSession,
463 ) -> Self {
464 let accumulators = Arc::new(Mutex::new(match dtype.as_struct_fields_opt() {
465 Some(struct_dtype) => {
466 if dtype.nullability() == Nullability::Nullable {
467 vortex_panic!(
469 "FileStatsAccumulator temporarily does not support nullable top-level structs, got: {}. Use Validity::NonNullable",
470 dtype
471 );
472 }
473
474 struct_dtype
475 .fields()
476 .map(|field_dtype| {
477 StatsAccumulator::new(
478 &field_dtype,
479 &stats,
480 max_variable_length_statistics_size,
481 )
482 })
483 .collect()
484 }
485 None => [StatsAccumulator::new(
486 dtype,
487 &stats,
488 max_variable_length_statistics_size,
489 )]
490 .into(),
491 }));
492
493 Self {
494 stats,
495 accumulators,
496 ctx: Arc::new(Mutex::new(session.create_execution_ctx())),
497 }
498 }
499
500 fn process(
501 &self,
502 chunk: VortexResult<(SequenceId, ArrayRef)>,
503 ) -> VortexResult<(SequenceId, ArrayRef)> {
504 let (sequence_id, chunk) = chunk?;
505 let mut ctx = self.ctx.lock();
506 if chunk.dtype().is_struct() {
507 let struct_chunk = chunk.clone().execute::<StructArray>(&mut ctx)?;
508 for (acc, field) in self
509 .accumulators
510 .lock()
511 .iter_mut()
512 .zip_eq(struct_chunk.iter_unmasked_fields())
513 {
514 acc.push_chunk(field, &mut ctx)?;
515 }
516 } else {
517 self.accumulators.lock()[0].push_chunk(&chunk, &mut ctx)?;
518 }
519 Ok((sequence_id, chunk))
520 }
521
522 pub fn stats_sets(&self) -> Vec<StatsSet> {
523 let mut ctx = self.ctx.lock();
524 self.accumulators
525 .lock()
526 .iter_mut()
527 .map(|acc| {
528 acc.as_stats_set(&self.stats, &mut ctx)
529 .vortex_expect("as_stats_table should not fail")
530 })
531 .collect()
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use rstest::rstest;
538 use vortex_array::IntoArray;
539 use vortex_array::array_session;
540 use vortex_array::arrays::BoolArray;
541 use vortex_array::arrays::PrimitiveArray;
542 use vortex_array::arrays::bool::BoolArrayExt;
543 use vortex_array::builders::VarBinViewBuilder;
544 use vortex_buffer::BitBuffer;
545 use vortex_buffer::buffer;
546
547 use super::*;
548
549 #[rstest]
550 #[case(DType::Utf8(Nullability::NonNullable))]
551 #[case(DType::Binary(Nullability::NonNullable))]
552 fn truncates_accumulated_stats(#[case] dtype: DType) {
553 let mut ctx = array_session().create_execution_ctx();
554 let mut builder = VarBinViewBuilder::with_capacity_in(
555 dtype.clone(),
556 2,
557 vortex_buffer::BufferAllocatorRef::statically_allocated(),
558 );
559 builder.append_value("Value to be truncated");
560 builder.append_value("untruncated");
561 let mut builder2 = VarBinViewBuilder::with_capacity_in(
562 dtype,
563 2,
564 vortex_buffer::BufferAllocatorRef::statically_allocated(),
565 );
566 builder2.append_value("Another");
567 builder2.append_value("wait a minute");
568 let mut acc =
569 StatsAccumulator::new(builder.dtype(), &[Stat::Max, Stat::Min, Stat::Sum], 12);
570 acc.push_chunk(&builder.finish(), &mut ctx)
571 .vortex_expect("push_chunk should succeed for test data");
572 acc.push_chunk(&builder2.finish(), &mut ctx)
573 .vortex_expect("push_chunk should succeed for test data");
574 let stats_table = acc
575 .as_array(&mut ctx)
576 .unwrap()
577 .expect("Must have stats table");
578 assert_eq!(
579 stats_table.names().as_ref(),
580 &[
581 Stat::Max.name(),
582 MAX_IS_TRUNCATED,
583 Stat::Min.name(),
584 MIN_IS_TRUNCATED,
585 ]
586 );
587 let field1_bool = stats_table
588 .unmasked_field(1)
589 .clone()
590 .execute::<BoolArray>(&mut ctx)
591 .unwrap();
592 assert_eq!(
593 field1_bool.to_bit_buffer(),
594 BitBuffer::from(vec![false, true])
595 );
596 let field3_bool = stats_table
597 .unmasked_field(3)
598 .clone()
599 .execute::<BoolArray>(&mut ctx)
600 .unwrap();
601 assert_eq!(
602 field3_bool.to_bit_buffer(),
603 BitBuffer::from(vec![true, false])
604 );
605 }
606
607 #[rstest]
608 #[case(DType::Utf8(Nullability::NonNullable))]
609 #[case(DType::Binary(Nullability::NonNullable))]
610 fn truncated_accumulated_stats_are_inexact(#[case] dtype: DType) {
611 let mut ctx = array_session().create_execution_ctx();
612 let mut builder = VarBinViewBuilder::with_capacity_in(
613 dtype,
614 2,
615 vortex_buffer::BufferAllocatorRef::statically_allocated(),
616 );
617 builder.append_value("Value to be truncated");
618 builder.append_value("Another truncated value");
619 let mut acc = StatsAccumulator::new(builder.dtype(), &[Stat::Max, Stat::Min], 12);
620 acc.push_chunk(&builder.finish(), &mut ctx)
621 .vortex_expect("push_chunk should succeed for test data");
622
623 let stats = acc
624 .as_stats_set(&[Stat::Max, Stat::Min], &mut ctx)
625 .vortex_expect("as_stats_set should succeed for test data");
626
627 assert!(matches!(stats.get(Stat::Min), Precision::Inexact(_)));
628 assert!(matches!(stats.get(Stat::Max), Precision::Inexact(_)));
629 }
630
631 #[test]
632 fn fixed_width_stats_omit_is_truncated_columns() {
633 let mut ctx = array_session().create_execution_ctx();
634 let array = buffer![0, 1, 2].into_array();
635 let mut acc = StatsAccumulator::new(array.dtype(), &[Stat::Max, Stat::Min, Stat::Sum], 12);
636 acc.push_chunk(&array, &mut ctx)
637 .vortex_expect("push_chunk should succeed for test array");
638 let stats_table = acc
639 .as_array(&mut ctx)
640 .unwrap()
641 .expect("Must have stats table");
642 assert_eq!(
643 stats_table.names().as_ref(),
644 &[Stat::Max.name(), Stat::Min.name(), Stat::Sum.name()]
645 );
646 }
647
648 #[rstest]
649 #[case::one_chunk_overflows(vec![vec![Some(i64::MAX), Some(1)], vec![Some(2)]], None)]
650 #[case::all_chunks_overflow(vec![vec![Some(i64::MAX), Some(1)]; 2], None)]
651 #[case::total_overflows(vec![vec![Some(i64::MAX)], vec![Some(1)]], None)]
652 #[case::nullable_values(vec![vec![None, Some(3)], vec![Some(4), None]], Some(7))]
653 #[case::all_null_chunk(vec![vec![None], vec![Some(5)]], Some(5))]
654 fn combines_chunk_sums(
655 #[case] chunks: Vec<Vec<Option<i64>>>,
656 #[case] expected: Option<i64>,
657 ) -> VortexResult<()> {
658 let mut ctx = array_session().create_execution_ctx();
659 let dtype = DType::Primitive(PType::I64, Nullability::Nullable);
660 let mut acc = StatsAccumulator::new(&dtype, &[Stat::Sum], 12);
661 for chunk in chunks {
662 acc.push_chunk(
663 &PrimitiveArray::from_option_iter(chunk).into_array(),
664 &mut ctx,
665 )?;
666 }
667
668 let stats = acc.as_stats_set(&[Stat::Sum], &mut ctx)?;
669 assert_eq!(
670 stats.get(Stat::Sum),
671 expected.map_or(Precision::Absent, Precision::exact)
672 );
673
674 Ok(())
675 }
676}