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