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::Min | Stat::Max | Stat::Sum => {
162 if let Some(s) = values.statistics().compute_stat(stat, ctx)?
163 && let Some(v) = s.into_value()
164 {
165 let precision = if stat_was_truncated(&stats_table, stat, ctx)? {
166 Precision::inexact(v)
167 } else {
168 Precision::exact(v)
169 };
170 stats_set.set(stat, precision)
171 }
172 }
173 Stat::NullCount | Stat::NaNCount | Stat::UncompressedSizeInBytes => {
174 if let Some(sum_value) = sum(values, ctx)?
175 .cast(&DType::Primitive(PType::U64, Nullability::Nullable))?
176 .into_value()
177 {
178 stats_set.set(stat, Precision::exact(sum_value));
179 }
180 }
181 Stat::IsConstant | Stat::IsSorted | Stat::IsStrictSorted => {}
182 }
183 }
184 Ok(stats_set)
185 }
186}
187
188fn stat_was_truncated(
189 stats_table: &StructArray,
190 stat: Stat,
191 ctx: &mut ExecutionCtx,
192) -> VortexResult<bool> {
193 let field_name = match stat {
194 Stat::Min => MIN_IS_TRUNCATED,
195 Stat::Max => MAX_IS_TRUNCATED,
196 _ => return Ok(false),
197 };
198 let Some(is_truncated) = stats_table.unmasked_field_by_name_opt(field_name) else {
199 return Ok(false);
200 };
201
202 Ok(is_truncated
203 .statistics()
204 .compute_stat(Stat::Max, ctx)?
205 .is_some_and(|max| max.as_bool().value() == Some(true)))
206}
207
208fn supports_file_stats(dtype: &DType) -> bool {
209 !matches!(dtype, DType::Variant(_))
210}
211
212fn is_varlen_dtype(dtype: &DType) -> bool {
213 matches!(dtype, DType::Utf8(_) | DType::Binary(_))
214}
215
216fn stats_builder_with_capacity(
217 stat: Stat,
218 dtype: &DType,
219 capacity: usize,
220 max_length: usize,
221) -> Box<dyn StatsArrayBuilder> {
222 let values_builder = builder_with_capacity_in(
223 dtype,
224 capacity,
225 vortex_buffer::BufferAllocatorRef::static_ref(),
226 );
227 match stat {
228 Stat::Max => match dtype {
229 DType::Utf8(_) => Box::new(TruncatedMaxBinaryStatsBuilder::<BufferString>::new(
230 values_builder,
231 BoolBuilder::with_capacity_in(
232 Nullability::NonNullable,
233 capacity,
234 vortex_buffer::BufferAllocatorRef::static_ref(),
235 ),
236 max_length,
237 )),
238 DType::Binary(_) => Box::new(TruncatedMaxBinaryStatsBuilder::<ByteBuffer>::new(
239 values_builder,
240 BoolBuilder::with_capacity_in(
241 Nullability::NonNullable,
242 capacity,
243 vortex_buffer::BufferAllocatorRef::static_ref(),
244 ),
245 max_length,
246 )),
247 _ => Box::new(StatNameArrayBuilder::new(stat, values_builder)),
248 },
249 Stat::Min => match dtype {
250 DType::Utf8(_) => Box::new(TruncatedMinBinaryStatsBuilder::<BufferString>::new(
251 values_builder,
252 BoolBuilder::with_capacity_in(
253 Nullability::NonNullable,
254 capacity,
255 vortex_buffer::BufferAllocatorRef::static_ref(),
256 ),
257 max_length,
258 )),
259 DType::Binary(_) => Box::new(TruncatedMinBinaryStatsBuilder::<ByteBuffer>::new(
260 values_builder,
261 BoolBuilder::with_capacity_in(
262 Nullability::NonNullable,
263 capacity,
264 vortex_buffer::BufferAllocatorRef::static_ref(),
265 ),
266 max_length,
267 )),
268 _ => Box::new(StatNameArrayBuilder::new(stat, values_builder)),
269 },
270 _ => Box::new(StatNameArrayBuilder::new(stat, values_builder)),
271 }
272}
273
274struct NamedArrays {
276 names: Vec<FieldName>,
277 arrays: Vec<ArrayRef>,
278}
279
280impl NamedArrays {
281 fn all_invalid(&self, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
282 self.arrays[0].all_invalid(ctx)
283 }
284}
285
286trait StatsArrayBuilder: Send {
287 fn stat(&self) -> Stat;
288
289 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()>;
290
291 fn append_null(&mut self);
292
293 fn finish(&mut self) -> NamedArrays;
294}
295
296struct StatNameArrayBuilder {
297 stat: Stat,
298 builder: Box<dyn ArrayBuilder>,
299}
300
301impl StatNameArrayBuilder {
302 fn new(stat: Stat, builder: Box<dyn ArrayBuilder>) -> Self {
303 Self { stat, builder }
304 }
305}
306
307impl StatsArrayBuilder for StatNameArrayBuilder {
308 fn stat(&self) -> Stat {
309 self.stat
310 }
311
312 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()> {
313 self.builder.append_scalar(&value)
314 }
315
316 fn append_null(&mut self) {
317 self.builder.append_null()
318 }
319
320 fn finish(&mut self) -> NamedArrays {
321 NamedArrays {
322 names: vec![self.stat.name().into()],
323 arrays: vec![self.builder.finish()],
324 }
325 }
326}
327
328struct TruncatedMaxBinaryStatsBuilder<T: ScalarTruncation> {
329 values: Box<dyn ArrayBuilder>,
330 is_truncated: BoolBuilder,
331 max_value_length: usize,
332 _marker: PhantomData<T>,
333}
334
335impl<T: ScalarTruncation> TruncatedMaxBinaryStatsBuilder<T> {
336 fn new(
337 values: Box<dyn ArrayBuilder>,
338 is_truncated: BoolBuilder,
339 max_value_length: usize,
340 ) -> Self {
341 Self {
342 values,
343 is_truncated,
344 max_value_length,
345 _marker: PhantomData,
346 }
347 }
348}
349
350struct TruncatedMinBinaryStatsBuilder<T: ScalarTruncation> {
351 values: Box<dyn ArrayBuilder>,
352 is_truncated: BoolBuilder,
353 max_value_length: usize,
354 _marker: PhantomData<T>,
355}
356
357impl<T: ScalarTruncation> TruncatedMinBinaryStatsBuilder<T> {
358 fn new(
359 values: Box<dyn ArrayBuilder>,
360 is_truncated: BoolBuilder,
361 max_value_length: usize,
362 ) -> Self {
363 Self {
364 values,
365 is_truncated,
366 max_value_length,
367 _marker: PhantomData,
368 }
369 }
370}
371
372impl<T: ScalarTruncation> StatsArrayBuilder for TruncatedMaxBinaryStatsBuilder<T> {
373 fn stat(&self) -> Stat {
374 Stat::Max
375 }
376
377 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()> {
378 let nullability = value.dtype().nullability();
379 if let Some((upper_bound, truncated)) =
380 upper_bound(T::from_scalar(value)?, self.max_value_length, nullability)
381 {
382 self.values.append_scalar(&upper_bound)?;
383 self.is_truncated.append_value(truncated);
384 } else {
385 self.append_null()
386 }
387 Ok(())
388 }
389
390 fn append_null(&mut self) {
391 ArrayBuilder::append_null(self.values.as_mut());
392 self.is_truncated.append_value(false);
393 }
394
395 fn finish(&mut self) -> NamedArrays {
396 NamedArrays {
397 names: vec![Stat::Max.name().into(), MAX_IS_TRUNCATED.into()],
398 arrays: vec![
399 ArrayBuilder::finish(self.values.as_mut()),
400 ArrayBuilder::finish(&mut self.is_truncated),
401 ],
402 }
403 }
404}
405
406impl<T: ScalarTruncation> StatsArrayBuilder for TruncatedMinBinaryStatsBuilder<T> {
407 fn stat(&self) -> Stat {
408 Stat::Min
409 }
410
411 fn append_scalar(&mut self, value: Scalar) -> VortexResult<()> {
412 let nullability = value.dtype().nullability();
413 if let Some((lower_bound, truncated)) =
414 lower_bound(T::from_scalar(value)?, self.max_value_length, nullability)
415 {
416 self.values.append_scalar(&lower_bound)?;
417 self.is_truncated.append_value(truncated);
418 } else {
419 self.append_null()
420 }
421 Ok(())
422 }
423
424 fn append_null(&mut self) {
425 ArrayBuilder::append_null(self.values.as_mut());
426 self.is_truncated.append_value(false);
427 }
428
429 fn finish(&mut self) -> NamedArrays {
430 NamedArrays {
431 names: vec![Stat::Min.name().into(), MIN_IS_TRUNCATED.into()],
432 arrays: vec![
433 ArrayBuilder::finish(self.values.as_mut()),
434 ArrayBuilder::finish(&mut self.is_truncated),
435 ],
436 }
437 }
438}
439
440#[derive(Clone)]
444pub struct FileStatsAccumulator {
445 stats: Arc<[Stat]>,
446 accumulators: Arc<Mutex<Vec<StatsAccumulator>>>,
447 ctx: Arc<Mutex<ExecutionCtx>>,
448}
449
450impl FileStatsAccumulator {
451 fn new(
452 dtype: &DType,
453 stats: Arc<[Stat]>,
454 max_variable_length_statistics_size: usize,
455 session: &VortexSession,
456 ) -> Self {
457 let accumulators = Arc::new(Mutex::new(match dtype.as_struct_fields_opt() {
458 Some(struct_dtype) => {
459 if dtype.nullability() == Nullability::Nullable {
460 vortex_panic!(
462 "FileStatsAccumulator temporarily does not support nullable top-level structs, got: {}. Use Validity::NonNullable",
463 dtype
464 );
465 }
466
467 struct_dtype
468 .fields()
469 .map(|field_dtype| {
470 StatsAccumulator::new(
471 &field_dtype,
472 &stats,
473 max_variable_length_statistics_size,
474 )
475 })
476 .collect()
477 }
478 None => [StatsAccumulator::new(
479 dtype,
480 &stats,
481 max_variable_length_statistics_size,
482 )]
483 .into(),
484 }));
485
486 Self {
487 stats,
488 accumulators,
489 ctx: Arc::new(Mutex::new(session.create_execution_ctx())),
490 }
491 }
492
493 fn process(
494 &self,
495 chunk: VortexResult<(SequenceId, ArrayRef)>,
496 ) -> VortexResult<(SequenceId, ArrayRef)> {
497 let (sequence_id, chunk) = chunk?;
498 let mut ctx = self.ctx.lock();
499 if chunk.dtype().is_struct() {
500 let struct_chunk = chunk.clone().execute::<StructArray>(&mut ctx)?;
501 for (acc, field) in self
502 .accumulators
503 .lock()
504 .iter_mut()
505 .zip_eq(struct_chunk.iter_unmasked_fields())
506 {
507 acc.push_chunk(field, &mut ctx)?;
508 }
509 } else {
510 self.accumulators.lock()[0].push_chunk(&chunk, &mut ctx)?;
511 }
512 Ok((sequence_id, chunk))
513 }
514
515 pub fn stats_sets(&self) -> Vec<StatsSet> {
516 let mut ctx = self.ctx.lock();
517 self.accumulators
518 .lock()
519 .iter_mut()
520 .map(|acc| {
521 acc.as_stats_set(&self.stats, &mut ctx)
522 .vortex_expect("as_stats_table should not fail")
523 })
524 .collect()
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use rstest::rstest;
531 use vortex_array::IntoArray;
532 use vortex_array::array_session;
533 use vortex_array::arrays::BoolArray;
534 use vortex_array::arrays::bool::BoolArrayExt;
535 use vortex_array::builders::VarBinViewBuilder;
536 use vortex_buffer::BitBuffer;
537 use vortex_buffer::buffer;
538
539 use super::*;
540
541 #[rstest]
542 #[case(DType::Utf8(Nullability::NonNullable))]
543 #[case(DType::Binary(Nullability::NonNullable))]
544 fn truncates_accumulated_stats(#[case] dtype: DType) {
545 let mut ctx = array_session().create_execution_ctx();
546 let mut builder = VarBinViewBuilder::with_capacity_in(
547 dtype.clone(),
548 2,
549 vortex_buffer::BufferAllocatorRef::statically_allocated(),
550 );
551 builder.append_value("Value to be truncated");
552 builder.append_value("untruncated");
553 let mut builder2 = VarBinViewBuilder::with_capacity_in(
554 dtype,
555 2,
556 vortex_buffer::BufferAllocatorRef::statically_allocated(),
557 );
558 builder2.append_value("Another");
559 builder2.append_value("wait a minute");
560 let mut acc =
561 StatsAccumulator::new(builder.dtype(), &[Stat::Max, Stat::Min, Stat::Sum], 12);
562 acc.push_chunk(&builder.finish(), &mut ctx)
563 .vortex_expect("push_chunk should succeed for test data");
564 acc.push_chunk(&builder2.finish(), &mut ctx)
565 .vortex_expect("push_chunk should succeed for test data");
566 let stats_table = acc
567 .as_array(&mut ctx)
568 .unwrap()
569 .expect("Must have stats table");
570 assert_eq!(
571 stats_table.names().as_ref(),
572 &[
573 Stat::Max.name(),
574 MAX_IS_TRUNCATED,
575 Stat::Min.name(),
576 MIN_IS_TRUNCATED,
577 ]
578 );
579 let field1_bool = stats_table
580 .unmasked_field(1)
581 .clone()
582 .execute::<BoolArray>(&mut ctx)
583 .unwrap();
584 assert_eq!(
585 field1_bool.to_bit_buffer(),
586 BitBuffer::from(vec![false, true])
587 );
588 let field3_bool = stats_table
589 .unmasked_field(3)
590 .clone()
591 .execute::<BoolArray>(&mut ctx)
592 .unwrap();
593 assert_eq!(
594 field3_bool.to_bit_buffer(),
595 BitBuffer::from(vec![true, false])
596 );
597 }
598
599 #[rstest]
600 #[case(DType::Utf8(Nullability::NonNullable))]
601 #[case(DType::Binary(Nullability::NonNullable))]
602 fn truncated_accumulated_stats_are_inexact(#[case] dtype: DType) {
603 let mut ctx = array_session().create_execution_ctx();
604 let mut builder = VarBinViewBuilder::with_capacity_in(
605 dtype,
606 2,
607 vortex_buffer::BufferAllocatorRef::statically_allocated(),
608 );
609 builder.append_value("Value to be truncated");
610 builder.append_value("Another truncated value");
611 let mut acc = StatsAccumulator::new(builder.dtype(), &[Stat::Max, Stat::Min], 12);
612 acc.push_chunk(&builder.finish(), &mut ctx)
613 .vortex_expect("push_chunk should succeed for test data");
614
615 let stats = acc
616 .as_stats_set(&[Stat::Max, Stat::Min], &mut ctx)
617 .vortex_expect("as_stats_set should succeed for test data");
618
619 assert!(matches!(stats.get(Stat::Min), Precision::Inexact(_)));
620 assert!(matches!(stats.get(Stat::Max), Precision::Inexact(_)));
621 }
622
623 #[test]
624 fn fixed_width_stats_omit_is_truncated_columns() {
625 let mut ctx = array_session().create_execution_ctx();
626 let array = buffer![0, 1, 2].into_array();
627 let mut acc = StatsAccumulator::new(array.dtype(), &[Stat::Max, Stat::Min, Stat::Sum], 12);
628 acc.push_chunk(&array, &mut ctx)
629 .vortex_expect("push_chunk should succeed for test array");
630 let stats_table = acc
631 .as_array(&mut ctx)
632 .unwrap()
633 .expect("Must have stats table");
634 assert_eq!(
635 stats_table.names().as_ref(),
636 &[Stat::Max.name(), Stat::Min.name(), Stat::Sum.name()]
637 );
638 }
639}