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