1use std::collections::BTreeSet;
11use std::ops::Range;
12use std::sync::Arc;
13
14use vortex_array::Canonical;
15use vortex_array::IntoArray;
16use vortex_array::MaskFuture;
17use vortex_array::VortexSessionExecute;
18use vortex_array::arrays::ConstantArray;
19use vortex_array::arrays::NullArray;
20use vortex_array::dtype::DType;
21use vortex_array::dtype::FieldMask;
22use vortex_array::dtype::FieldPath;
23use vortex_array::dtype::StructFields;
24use vortex_array::expr::Expression;
25use vortex_array::expr::StatsCatalog;
26use vortex_array::expr::lit;
27use vortex_array::expr::stats::Stat;
28use vortex_array::scalar::Scalar;
29use vortex_array::scalar_fn::fns::literal::Literal;
30use vortex_array::scalar_fn::internal::row_count::substitute_row_count;
31use vortex_error::VortexResult;
32use vortex_layout::ArrayFuture;
33use vortex_layout::LayoutReader;
34use vortex_layout::LayoutReaderRef;
35use vortex_mask::Mask;
36use vortex_session::VortexSession;
37use vortex_utils::aliases::dash_map::DashMap;
38
39use crate::FileStatistics;
40
41pub struct FileStatsLayoutReader {
50 child: LayoutReaderRef,
51 file_stats: FileStatistics,
52 struct_fields: StructFields,
53 session: VortexSession,
54 prune_cache: DashMap<Expression, bool>,
55}
56
57impl FileStatsLayoutReader {
58 pub fn new(child: LayoutReaderRef, file_stats: FileStatistics, session: VortexSession) -> Self {
65 let struct_fields = child
66 .dtype()
67 .as_struct_fields_opt()
68 .cloned()
69 .unwrap_or_default();
70
71 Self {
72 child,
73 file_stats,
74 struct_fields,
75 session,
76 prune_cache: Default::default(),
77 }
78 }
79
80 fn evaluate_file_stats(&self, expr: &Expression) -> VortexResult<bool> {
85 let Some(pruning_expr) = expr.stat_falsification(self) else {
86 return Ok(false);
88 };
89
90 let simplified = pruning_expr.optimize_recursive(&DType::Null)?;
94 if let Some(result) = simplified.as_opt::<Literal>() {
95 return Ok(result.as_bool().value() == Some(true));
97 }
98
99 let pruning = NullArray::new(1).into_array().apply(&pruning_expr)?;
103 let row_count_replacement =
104 ConstantArray::new(self.child.row_count(), pruning.len()).into_array();
105 let pruning = substitute_row_count(pruning, &row_count_replacement)?;
106
107 let mut ctx = self.session.create_execution_ctx();
108 let result = pruning
109 .execute::<Canonical>(&mut ctx)?
110 .into_bool()
111 .into_array()
112 .execute_scalar(0, &mut ctx)?;
113
114 Ok(result.as_bool().value() == Some(true))
115 }
116
117 pub fn file_stats(&self) -> &FileStatistics {
118 &self.file_stats
119 }
120}
121
122impl StatsCatalog for FileStatsLayoutReader {
124 fn stats_ref(&self, field_path: &FieldPath, stat: Stat) -> Option<Expression> {
125 if field_path.parts().len() != 1 {
127 return None;
128 }
129
130 let field_name = field_path.parts()[0].as_name()?;
131 let field_idx = self.struct_fields.find(field_name)?;
132 let field_stats = self.file_stats.stats_sets().get(field_idx)?;
133
134 let stat_value = field_stats.get(stat)?.as_exact()?;
135 let field_dtype = self.struct_fields.field_by_index(field_idx)?;
136 let stat_dtype = stat.dtype(&field_dtype)?;
137 let stat_scalar = Scalar::try_new(stat_dtype, Some(stat_value)).ok()?;
138
139 Some(lit(stat_scalar))
140 }
141}
142
143impl LayoutReader for FileStatsLayoutReader {
144 fn name(&self) -> &Arc<str> {
145 self.child.name()
146 }
147
148 fn dtype(&self) -> &DType {
149 self.child.dtype()
150 }
151
152 fn row_count(&self) -> u64 {
153 self.child.row_count()
154 }
155
156 fn register_splits(
157 &self,
158 field_mask: &[FieldMask],
159 row_range: &Range<u64>,
160 splits: &mut BTreeSet<u64>,
161 ) -> VortexResult<()> {
162 self.child.register_splits(field_mask, row_range, splits)
163 }
164
165 fn pruning_evaluation(
166 &self,
167 row_range: &Range<u64>,
168 expr: &Expression,
169 mask: Mask,
170 ) -> VortexResult<MaskFuture> {
171 if let Some(pruned) = self.prune_cache.get(expr) {
173 if *pruned {
174 return Ok(MaskFuture::ready(Mask::new_false(mask.len())));
175 }
176 return self.child.pruning_evaluation(row_range, expr, mask);
177 }
178
179 let pruned = self.evaluate_file_stats(expr)?;
181 self.prune_cache.insert(expr.clone(), pruned);
182
183 if pruned {
184 Ok(MaskFuture::ready(Mask::new_false(mask.len())))
185 } else {
186 self.child.pruning_evaluation(row_range, expr, mask)
187 }
188 }
189
190 fn filter_evaluation(
191 &self,
192 row_range: &Range<u64>,
193 expr: &Expression,
194 mask: MaskFuture,
195 ) -> VortexResult<MaskFuture> {
196 self.child.filter_evaluation(row_range, expr, mask)
197 }
198
199 fn projection_evaluation(
200 &self,
201 row_range: &Range<u64>,
202 expr: &Expression,
203 mask: MaskFuture,
204 ) -> VortexResult<ArrayFuture> {
205 self.child.projection_evaluation(row_range, expr, mask)
206 }
207
208 fn as_any(&self) -> &dyn std::any::Any {
209 self
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use std::sync::Arc;
216 use std::sync::LazyLock;
217
218 use vortex_array::ArrayContext;
219 use vortex_array::IntoArray as _;
220 use vortex_array::arrays::PrimitiveArray;
221 use vortex_array::arrays::StructArray;
222 use vortex_array::arrays::datetime::TemporalData;
223 use vortex_array::dtype::DType;
224 use vortex_array::dtype::Nullability;
225 use vortex_array::dtype::PType;
226 use vortex_array::expr::get_item;
227 use vortex_array::expr::gt;
228 use vortex_array::expr::is_not_null;
229 use vortex_array::expr::is_null;
230 use vortex_array::expr::lit;
231 use vortex_array::expr::root;
232 use vortex_array::expr::stats::Precision;
233 use vortex_array::expr::stats::Stat;
234 use vortex_array::extension::datetime::TimeUnit;
235 use vortex_array::scalar::ScalarValue;
236 use vortex_array::scalar_fn::session::ScalarFnSession;
237 use vortex_array::session::ArraySession;
238 use vortex_array::stats::StatsSet;
239 use vortex_buffer::buffer;
240 use vortex_error::VortexResult;
241 use vortex_io::runtime::single::block_on;
242 use vortex_io::session::RuntimeSession;
243 use vortex_io::session::RuntimeSessionExt;
244 use vortex_layout::LayoutReader;
245 use vortex_layout::LayoutStrategy;
246 use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
247 use vortex_layout::layouts::table::TableStrategy;
248 use vortex_layout::segments::SegmentSink;
249 use vortex_layout::segments::TestSegments;
250 use vortex_layout::sequence::SequenceId;
251 use vortex_layout::sequence::SequentialArrayStreamExt;
252 use vortex_layout::session::LayoutSession;
253 use vortex_mask::Mask;
254 use vortex_session::VortexSession;
255
256 use super::*;
257
258 static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
259 VortexSession::empty()
260 .with::<ArraySession>()
261 .with::<LayoutSession>()
262 .with::<ScalarFnSession>()
263 .with::<RuntimeSession>()
264 });
265
266 fn test_file_stats(min: i32, max: i32) -> FileStatistics {
267 let mut stats = StatsSet::default();
268 stats.set(Stat::Min, Precision::exact(ScalarValue::from(min)));
269 stats.set(Stat::Max, Precision::exact(ScalarValue::from(max)));
270 FileStatistics::new(
271 Arc::from([stats]),
272 Arc::from([DType::Primitive(PType::I32, Nullability::NonNullable)]),
273 )
274 }
275
276 fn test_file_null_count_stats(null_count: u64) -> FileStatistics {
277 let mut stats = StatsSet::default();
278 stats.set(
279 Stat::NullCount,
280 Precision::exact(ScalarValue::from(null_count)),
281 );
282 FileStatistics::new(
283 Arc::from([stats]),
284 Arc::from([DType::Primitive(PType::I32, Nullability::Nullable)]),
285 )
286 }
287
288 #[test]
289 fn pruning_when_filter_out_of_range() -> VortexResult<()> {
290 block_on(|handle| async {
291 let session = SESSION.clone().with_handle(handle);
292 let ctx = ArrayContext::empty();
293 let segments = Arc::new(TestSegments::default());
294 let (ptr, eof) = SequenceId::root().split();
295 let struct_array = StructArray::from_fields(
296 [("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
297 )?;
298 let strategy = TableStrategy::new(
299 Arc::new(FlatLayoutStrategy::default()),
300 Arc::new(FlatLayoutStrategy::default()),
301 );
302 let layout = strategy
303 .write_stream(
304 ctx,
305 Arc::<TestSegments>::clone(&segments),
306 struct_array.into_array().to_array_stream().sequenced(ptr),
307 eof,
308 &session,
309 )
310 .await?;
311
312 let child = layout.new_reader("".into(), segments, &SESSION)?;
313
314 let reader =
315 FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
316
317 let expr = gt(get_item("col", root()), lit(200i32));
319 let mask = Mask::new_true(5);
320 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
321 assert_eq!(result, Mask::new_false(5));
322
323 Ok(())
324 })
325 }
326
327 #[test]
328 fn no_pruning_when_filter_in_range() -> VortexResult<()> {
329 block_on(|handle| async {
330 let session = SESSION.clone().with_handle(handle);
331 let ctx = ArrayContext::empty();
332 let segments = Arc::new(TestSegments::default());
333 let (ptr, eof) = SequenceId::root().split();
334 let struct_array = StructArray::from_fields(
335 [("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
336 )?;
337 let strategy = TableStrategy::new(
338 Arc::new(FlatLayoutStrategy::default()),
339 Arc::new(FlatLayoutStrategy::default()),
340 );
341 let layout = strategy
342 .write_stream(
343 ctx,
344 Arc::<TestSegments>::clone(&segments),
345 struct_array.into_array().to_array_stream().sequenced(ptr),
346 eof,
347 &session,
348 )
349 .await?;
350
351 let child = layout.new_reader("".into(), segments, &SESSION)?;
352
353 let reader =
354 FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
355
356 let expr = gt(get_item("col", root()), lit(50i32));
358 let mask = Mask::new_true(5);
359 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
360 assert_eq!(result, Mask::new_true(5));
362
363 Ok(())
364 })
365 }
366
367 #[test]
371 fn is_null_pruning_on_nullable_timestamp_column() -> VortexResult<()> {
372 block_on(|handle| async {
373 let session = SESSION.clone().with_handle(handle);
374 let ctx = ArrayContext::empty();
375 let segments = Arc::new(TestSegments::default());
376 let (ptr, eof) = SequenceId::root().split();
377
378 let prim_array =
380 PrimitiveArray::from_option_iter([Some(1_000_000i64), None, Some(3_000_000)])
381 .into_array();
382 let ts_data = TemporalData::new_timestamp(prim_array, TimeUnit::Microseconds, None);
383 let ts_dtype = ts_data.dtype().clone();
384 let ts_array = ts_data.into_array();
385
386 let struct_array = StructArray::from_fields([("deleted_at", ts_array)].as_slice())?;
387
388 let strategy = TableStrategy::new(
389 Arc::new(FlatLayoutStrategy::default()),
390 Arc::new(FlatLayoutStrategy::default()),
391 );
392 let layout = strategy
393 .write_stream(
394 ctx,
395 Arc::clone(&segments) as Arc<dyn SegmentSink>,
396 struct_array.into_array().to_array_stream().sequenced(ptr),
397 eof,
398 &session,
399 )
400 .await?;
401
402 let child = layout.new_reader("".into(), segments, &SESSION)?;
403
404 let mut stats = StatsSet::default();
406 stats.set(Stat::NullCount, Precision::exact(ScalarValue::from(1u64)));
407 let file_stats = FileStatistics::new(Arc::from([stats]), Arc::from([ts_dtype]));
408
409 let reader = FileStatsLayoutReader::new(child, file_stats, SESSION.clone());
410
411 let expr = is_null(get_item("deleted_at", root()));
413 let mask = Mask::new_true(3);
414 let result = reader.pruning_evaluation(&(0..3), &expr, mask)?.await?;
415 assert_eq!(result, Mask::new_true(3));
417
418 Ok(())
419 })
420 }
421
422 #[test]
423 fn pruning_is_not_null_when_file_is_all_null() -> VortexResult<()> {
424 block_on(|handle| async {
425 let session = SESSION.clone().with_handle(handle);
426 let ctx = ArrayContext::empty();
427 let segments = Arc::new(TestSegments::default());
428 let (ptr, eof) = SequenceId::root().split();
429 let struct_array = StructArray::from_fields(
430 [(
431 "col",
432 PrimitiveArray::from_option_iter([None::<i32>, None, None, None, None])
433 .into_array(),
434 )]
435 .as_slice(),
436 )?;
437 let strategy = TableStrategy::new(
438 Arc::new(FlatLayoutStrategy::default()),
439 Arc::new(FlatLayoutStrategy::default()),
440 );
441 let layout = strategy
442 .write_stream(
443 ctx,
444 Arc::clone(&segments) as Arc<dyn SegmentSink>,
445 struct_array.into_array().to_array_stream().sequenced(ptr),
446 eof,
447 &session,
448 )
449 .await?;
450
451 let child = layout.new_reader("".into(), segments, &SESSION)?;
452
453 let reader =
454 FileStatsLayoutReader::new(child, test_file_null_count_stats(5), SESSION.clone());
455
456 let expr = is_not_null(get_item("col", root()));
457 let mask = Mask::new_true(5);
458 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
459 assert_eq!(result, Mask::new_false(5));
460
461 Ok(())
462 })
463 }
464}