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