1use std::ops::Range;
11use std::sync::Arc;
12
13use vortex_array::MaskFuture;
14use vortex_array::dtype::DType;
15use vortex_array::dtype::FieldMask;
16use vortex_array::dtype::StructFields;
17use vortex_array::expr::BoundExpression;
18use vortex_array::expr::ExactBoundExpr;
19use vortex_error::VortexResult;
20use vortex_layout::ArrayFuture;
21use vortex_layout::LayoutReader;
22use vortex_layout::LayoutReaderRef;
23use vortex_layout::RowSplits;
24use vortex_layout::SplitRange;
25use vortex_mask::Mask;
26use vortex_session::VortexSession;
27use vortex_utils::aliases::dash_map::DashMap;
28use vortex_utils::aliases::dash_map::Entry;
29
30use crate::FileStatistics;
31use crate::pruning::can_prune_file_stats;
32
33pub struct FileStatsLayoutReader {
42 child: LayoutReaderRef,
43 file_stats: FileStatistics,
44 struct_fields: StructFields,
45 session: VortexSession,
46 prune_cache: DashMap<ExactBoundExpr, bool>,
47}
48
49impl FileStatsLayoutReader {
50 pub fn new(child: LayoutReaderRef, file_stats: FileStatistics, session: VortexSession) -> Self {
57 let struct_fields = child
58 .dtype()
59 .as_struct_fields_opt()
60 .cloned()
61 .unwrap_or_default();
62
63 Self {
64 child,
65 file_stats,
66 struct_fields,
67 session,
68 prune_cache: DashMap::with_hasher_and_shard_amount(Default::default(), 8),
69 }
70 }
71
72 fn evaluate_file_stats(&self, expr: &BoundExpression) -> VortexResult<bool> {
77 can_prune_file_stats(
78 expr,
79 self.child.row_count(),
80 &self.file_stats,
81 &self.struct_fields,
82 &self.session,
83 )
84 }
85
86 pub fn file_stats(&self) -> &FileStatistics {
88 &self.file_stats
89 }
90}
91
92impl LayoutReader for FileStatsLayoutReader {
93 fn name(&self) -> &Arc<str> {
94 self.child.name()
95 }
96
97 fn dtype(&self) -> &DType {
98 self.child.dtype()
99 }
100
101 fn row_count(&self) -> u64 {
102 self.child.row_count()
103 }
104
105 fn register_splits(
106 &self,
107 field_mask: &[FieldMask],
108 split_range: &SplitRange,
109 splits: &mut RowSplits,
110 ) -> VortexResult<()> {
111 self.child.register_splits(field_mask, split_range, splits)
112 }
113
114 fn pruning_evaluation(
115 &self,
116 row_range: &Range<u64>,
117 expr: &BoundExpression,
118 mask: Mask,
119 ) -> VortexResult<MaskFuture> {
120 let key = ExactBoundExpr(expr.clone());
121
122 let cached = self.prune_cache.get(&key).map(|pruned| *pruned);
124 let pruned = match cached {
125 Some(pruned) => pruned,
126 None => match self.prune_cache.entry(key) {
129 Entry::Occupied(entry) => *entry.get(),
130 Entry::Vacant(entry) => *entry.insert(self.evaluate_file_stats(expr)?),
131 },
132 };
133
134 if pruned {
135 Ok(MaskFuture::ready(Mask::new_false(mask.len())))
136 } else {
137 self.child.pruning_evaluation(row_range, expr, mask)
138 }
139 }
140
141 fn filter_evaluation(
142 &self,
143 row_range: &Range<u64>,
144 expr: &BoundExpression,
145 mask: MaskFuture,
146 ) -> VortexResult<MaskFuture> {
147 self.child.filter_evaluation(row_range, expr, mask)
148 }
149
150 fn projection_evaluation(
151 &self,
152 row_range: &Range<u64>,
153 expr: &BoundExpression,
154 mask: MaskFuture,
155 ) -> VortexResult<ArrayFuture> {
156 self.child.projection_evaluation(row_range, expr, mask)
157 }
158
159 fn as_any(&self) -> &dyn std::any::Any {
160 self
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use std::sync::Arc;
167 use std::sync::LazyLock;
168
169 use vortex_array::ArrayContext;
170 use vortex_array::IntoArray as _;
171 use vortex_array::arrays::PrimitiveArray;
172 use vortex_array::arrays::StructArray;
173 use vortex_array::arrays::datetime::TemporalData;
174 use vortex_array::dtype::DType;
175 use vortex_array::dtype::Nullability;
176 use vortex_array::dtype::PType;
177 use vortex_array::expr::checked_add;
178 use vortex_array::expr::get_item;
179 use vortex_array::expr::gt;
180 use vortex_array::expr::is_not_null;
181 use vortex_array::expr::is_null;
182 use vortex_array::expr::lit;
183 use vortex_array::expr::root;
184 use vortex_array::expr::stats::Precision;
185 use vortex_array::expr::stats::Stat;
186 use vortex_array::extension::datetime::TimeUnit;
187 use vortex_array::scalar::ScalarValue;
188 use vortex_array::stats::StatsSet;
189 use vortex_buffer::buffer;
190 use vortex_error::VortexResult;
191 use vortex_io::runtime::single::block_on;
192 use vortex_io::session::RuntimeSession;
193 use vortex_io::session::RuntimeSessionExt;
194 use vortex_layout::LayoutReader;
195 use vortex_layout::LayoutStrategy;
196 use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
197 use vortex_layout::layouts::table::TableStrategy;
198 use vortex_layout::segments::SegmentSink;
199 use vortex_layout::segments::TestSegments;
200 use vortex_layout::sequence::SequenceId;
201 use vortex_layout::sequence::SequentialArrayStreamExt;
202 use vortex_layout::session::LayoutSession;
203 use vortex_mask::Mask;
204 use vortex_session::VortexSession;
205
206 use super::*;
207
208 static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
209 vortex_array::array_session()
210 .with::<LayoutSession>()
211 .with::<RuntimeSession>()
212 });
213
214 fn test_file_stats(min: i32, max: i32) -> FileStatistics {
215 let mut stats = StatsSet::default();
216 stats.set(Stat::Min, Precision::exact(ScalarValue::from(min)));
217 stats.set(Stat::Max, Precision::exact(ScalarValue::from(max)));
218 FileStatistics::new(
219 Arc::from([stats]),
220 Arc::from([DType::Primitive(PType::I32, Nullability::NonNullable)]),
221 )
222 }
223
224 fn test_file_null_count_stats(null_count: u64) -> FileStatistics {
225 let mut stats = StatsSet::default();
226 stats.set(
227 Stat::NullCount,
228 Precision::exact(ScalarValue::from(null_count)),
229 );
230 FileStatistics::new(
231 Arc::from([stats]),
232 Arc::from([DType::Primitive(PType::I32, Nullability::Nullable)]),
233 )
234 }
235
236 #[test]
237 fn pruning_when_filter_out_of_range() -> VortexResult<()> {
238 block_on(|handle| async {
239 let session = SESSION.clone().with_handle(handle);
240 let ctx = ArrayContext::empty();
241 let segments = Arc::new(TestSegments::default());
242 let (ptr, eof) = SequenceId::root().split();
243 let struct_array = StructArray::from_fields(
244 [("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
245 )?;
246 let strategy = TableStrategy::new(
247 Arc::new(FlatLayoutStrategy::default()),
248 Arc::new(FlatLayoutStrategy::default()),
249 );
250 let layout = strategy
251 .write_stream(
252 ctx.into(),
253 Arc::<TestSegments>::clone(&segments),
254 struct_array.into_array().to_array_stream().sequenced(ptr),
255 eof,
256 &session,
257 )
258 .await?;
259
260 let child = layout.new_reader("".into(), segments, &SESSION, &Default::default())?;
261
262 let reader =
263 FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
264
265 let expr = gt(get_item("col", root()), lit(200i32)).bind(reader.dtype())?;
267 let mask = Mask::new_true(5);
268 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
269 assert_eq!(result, Mask::new_false(5));
270
271 Ok(())
272 })
273 }
274
275 #[test]
276 fn no_pruning_when_filter_in_range() -> VortexResult<()> {
277 block_on(|handle| async {
278 let session = SESSION.clone().with_handle(handle);
279 let ctx = ArrayContext::empty();
280 let segments = Arc::new(TestSegments::default());
281 let (ptr, eof) = SequenceId::root().split();
282 let struct_array = StructArray::from_fields(
283 [("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
284 )?;
285 let strategy = TableStrategy::new(
286 Arc::new(FlatLayoutStrategy::default()),
287 Arc::new(FlatLayoutStrategy::default()),
288 );
289 let layout = strategy
290 .write_stream(
291 ctx.into(),
292 Arc::<TestSegments>::clone(&segments),
293 struct_array.into_array().to_array_stream().sequenced(ptr),
294 eof,
295 &session,
296 )
297 .await?;
298
299 let child = layout.new_reader("".into(), segments, &SESSION, &Default::default())?;
300
301 let reader =
302 FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
303
304 let expr = gt(get_item("col", root()), lit(50i32)).bind(reader.dtype())?;
306 let mask = Mask::new_true(5);
307 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
308 assert_eq!(result, Mask::new_true(5));
310
311 Ok(())
312 })
313 }
314
315 #[test]
316 fn no_pruning_for_computed_expression_stats() -> VortexResult<()> {
317 block_on(|handle| async {
318 let session = SESSION.clone().with_handle(handle);
319 let ctx = ArrayContext::empty();
320 let segments = Arc::new(TestSegments::default());
321 let (ptr, eof) = SequenceId::root().split();
322 let struct_array =
323 StructArray::from_fields([("col", buffer![0i32, 100].into_array())].as_slice())?;
324 let strategy = TableStrategy::new(
325 Arc::new(FlatLayoutStrategy::default()),
326 Arc::new(FlatLayoutStrategy::default()),
327 );
328 let layout = strategy
329 .write_stream(
330 ctx.into(),
331 Arc::<TestSegments>::clone(&segments),
332 struct_array.into_array().to_array_stream().sequenced(ptr),
333 eof,
334 &session,
335 )
336 .await?;
337
338 let child = layout.new_reader("".into(), segments, &SESSION, &Default::default())?;
339 let reader =
340 FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
341
342 let expr = gt(checked_add(get_item("col", root()), lit(5i32)), lit(102i32))
343 .bind(reader.dtype())?;
344 let mask = Mask::new_true(2);
345 let result = reader.pruning_evaluation(&(0..2), &expr, mask)?.await?;
346
347 assert_eq!(result, Mask::new_true(2));
348
349 Ok(())
350 })
351 }
352
353 #[test]
357 fn is_null_pruning_on_nullable_timestamp_column() -> VortexResult<()> {
358 block_on(|handle| async {
359 let session = SESSION.clone().with_handle(handle);
360 let ctx = ArrayContext::empty();
361 let segments = Arc::new(TestSegments::default());
362 let (ptr, eof) = SequenceId::root().split();
363
364 let prim_array =
366 PrimitiveArray::from_option_iter([Some(1_000_000i64), None, Some(3_000_000)])
367 .into_array();
368 let ts_data = TemporalData::new_timestamp(prim_array, TimeUnit::Microseconds, None);
369 let ts_dtype = ts_data.dtype().clone();
370 let ts_array = ts_data.into_array();
371
372 let struct_array = StructArray::from_fields([("deleted_at", ts_array)].as_slice())?;
373
374 let strategy = TableStrategy::new(
375 Arc::new(FlatLayoutStrategy::default()),
376 Arc::new(FlatLayoutStrategy::default()),
377 );
378 let layout = strategy
379 .write_stream(
380 ctx.into(),
381 Arc::clone(&segments) as Arc<dyn SegmentSink>,
382 struct_array.into_array().to_array_stream().sequenced(ptr),
383 eof,
384 &session,
385 )
386 .await?;
387
388 let child = layout.new_reader("".into(), segments, &SESSION, &Default::default())?;
389
390 let mut stats = StatsSet::default();
392 stats.set(Stat::NullCount, Precision::exact(ScalarValue::from(1u64)));
393 let file_stats = FileStatistics::new(Arc::from([stats]), Arc::from([ts_dtype]));
394
395 let reader = FileStatsLayoutReader::new(child, file_stats, SESSION.clone());
396
397 let expr = is_null(get_item("deleted_at", root())).bind(reader.dtype())?;
399 let mask = Mask::new_true(3);
400 let result = reader.pruning_evaluation(&(0..3), &expr, mask)?.await?;
401 assert_eq!(result, Mask::new_true(3));
403
404 Ok(())
405 })
406 }
407
408 #[test]
409 fn pruning_is_not_null_when_file_is_all_null() -> VortexResult<()> {
410 block_on(|handle| async {
411 let session = SESSION.clone().with_handle(handle);
412 let ctx = ArrayContext::empty();
413 let segments = Arc::new(TestSegments::default());
414 let (ptr, eof) = SequenceId::root().split();
415 let struct_array = StructArray::from_fields(
416 [(
417 "col",
418 PrimitiveArray::from_option_iter([None::<i32>, None, None, None, None])
419 .into_array(),
420 )]
421 .as_slice(),
422 )?;
423 let strategy = TableStrategy::new(
424 Arc::new(FlatLayoutStrategy::default()),
425 Arc::new(FlatLayoutStrategy::default()),
426 );
427 let layout = strategy
428 .write_stream(
429 ctx.into(),
430 Arc::clone(&segments) as Arc<dyn SegmentSink>,
431 struct_array.into_array().to_array_stream().sequenced(ptr),
432 eof,
433 &session,
434 )
435 .await?;
436
437 let child = layout.new_reader("".into(), segments, &SESSION, &Default::default())?;
438
439 let reader =
440 FileStatsLayoutReader::new(child, test_file_null_count_stats(5), SESSION.clone());
441
442 let expr = is_not_null(get_item("col", root())).bind(reader.dtype())?;
443 let mask = Mask::new_true(5);
444 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
445 assert_eq!(result, Mask::new_false(5));
446
447 Ok(())
448 })
449 }
450}