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::NullArray;
19use vortex_array::dtype::DType;
20use vortex_array::dtype::FieldMask;
21use vortex_array::dtype::FieldPath;
22use vortex_array::dtype::StructFields;
23use vortex_array::expr::Expression;
24use vortex_array::expr::StatsCatalog;
25use vortex_array::expr::lit;
26use vortex_array::expr::stats::Stat;
27use vortex_array::scalar::Scalar;
28use vortex_array::scalar_fn::fns::literal::Literal;
29use vortex_error::VortexResult;
30use vortex_layout::ArrayFuture;
31use vortex_layout::LayoutReader;
32use vortex_layout::LayoutReaderRef;
33use vortex_mask::Mask;
34use vortex_session::VortexSession;
35use vortex_utils::aliases::dash_map::DashMap;
36
37use crate::FileStatistics;
38
39pub struct FileStatsLayoutReader {
48 child: LayoutReaderRef,
49 file_stats: FileStatistics,
50 struct_fields: StructFields,
51 session: VortexSession,
52 prune_cache: DashMap<Expression, bool>,
53}
54
55impl FileStatsLayoutReader {
56 pub fn new(child: LayoutReaderRef, file_stats: FileStatistics, session: VortexSession) -> Self {
63 let struct_fields = child
64 .dtype()
65 .as_struct_fields_opt()
66 .cloned()
67 .unwrap_or_default();
68
69 Self {
70 child,
71 file_stats,
72 struct_fields,
73 session,
74 prune_cache: Default::default(),
75 }
76 }
77
78 fn evaluate_file_stats(&self, expr: &Expression) -> VortexResult<bool> {
82 let Some(pruning_expr) = expr.stat_falsification(self) else {
83 return Ok(false);
85 };
86
87 let simplified = pruning_expr.optimize_recursive(&DType::Null)?;
91 if let Some(result) = simplified.as_opt::<Literal>() {
92 return Ok(result.as_bool().value() == Some(true));
94 }
95
96 let pruning = NullArray::new(1).into_array().apply(&pruning_expr)?;
99
100 let mut ctx = self.session.create_execution_ctx();
101 let result = pruning
102 .execute::<Canonical>(&mut ctx)?
103 .into_bool()
104 .into_array()
105 .execute_scalar(0, &mut ctx)?;
106
107 Ok(result.as_bool().value() == Some(true))
108 }
109}
110
111impl StatsCatalog for FileStatsLayoutReader {
113 fn stats_ref(&self, field_path: &FieldPath, stat: Stat) -> Option<Expression> {
114 if field_path.parts().len() != 1 {
116 return None;
117 }
118
119 let field_name = field_path.parts()[0].as_name()?;
120 let field_idx = self.struct_fields.find(field_name)?;
121 let field_stats = self.file_stats.stats_sets().get(field_idx)?;
122
123 let stat_value = field_stats.get(stat)?.as_exact()?;
124 let field_dtype = self.struct_fields.field_by_index(field_idx)?;
125 let stat_scalar = Scalar::try_new(field_dtype, Some(stat_value)).ok()?;
126
127 Some(lit(stat_scalar))
128 }
129}
130
131impl LayoutReader for FileStatsLayoutReader {
132 fn name(&self) -> &Arc<str> {
133 self.child.name()
134 }
135
136 fn dtype(&self) -> &DType {
137 self.child.dtype()
138 }
139
140 fn row_count(&self) -> u64 {
141 self.child.row_count()
142 }
143
144 fn register_splits(
145 &self,
146 field_mask: &[FieldMask],
147 row_range: &Range<u64>,
148 splits: &mut BTreeSet<u64>,
149 ) -> VortexResult<()> {
150 self.child.register_splits(field_mask, row_range, splits)
151 }
152
153 fn pruning_evaluation(
154 &self,
155 row_range: &Range<u64>,
156 expr: &Expression,
157 mask: Mask,
158 ) -> VortexResult<MaskFuture> {
159 if let Some(pruned) = self.prune_cache.get(expr) {
161 if *pruned {
162 return Ok(MaskFuture::ready(Mask::new_false(mask.len())));
163 }
164 return self.child.pruning_evaluation(row_range, expr, mask);
165 }
166
167 let pruned = self.evaluate_file_stats(expr)?;
169 self.prune_cache.insert(expr.clone(), pruned);
170
171 if pruned {
172 Ok(MaskFuture::ready(Mask::new_false(mask.len())))
173 } else {
174 self.child.pruning_evaluation(row_range, expr, mask)
175 }
176 }
177
178 fn filter_evaluation(
179 &self,
180 row_range: &Range<u64>,
181 expr: &Expression,
182 mask: MaskFuture,
183 ) -> VortexResult<MaskFuture> {
184 self.child.filter_evaluation(row_range, expr, mask)
185 }
186
187 fn projection_evaluation(
188 &self,
189 row_range: &Range<u64>,
190 expr: &Expression,
191 mask: MaskFuture,
192 ) -> VortexResult<ArrayFuture> {
193 self.child.projection_evaluation(row_range, expr, mask)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use std::sync::Arc;
200 use std::sync::LazyLock;
201
202 use vortex_array::ArrayContext;
203 use vortex_array::IntoArray as _;
204 use vortex_array::arrays::StructArray;
205 use vortex_array::dtype::DType;
206 use vortex_array::dtype::Nullability;
207 use vortex_array::dtype::PType;
208 use vortex_array::expr::get_item;
209 use vortex_array::expr::gt;
210 use vortex_array::expr::lit;
211 use vortex_array::expr::root;
212 use vortex_array::expr::stats::Precision;
213 use vortex_array::expr::stats::Stat;
214 use vortex_array::scalar::ScalarValue;
215 use vortex_array::scalar_fn::session::ScalarFnSession;
216 use vortex_array::session::ArraySession;
217 use vortex_array::stats::StatsSet;
218 use vortex_buffer::buffer;
219 use vortex_error::VortexResult;
220 use vortex_io::runtime::single::block_on;
221 use vortex_io::session::RuntimeSession;
222 use vortex_io::session::RuntimeSessionExt;
223 use vortex_layout::LayoutReader;
224 use vortex_layout::LayoutStrategy;
225 use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
226 use vortex_layout::layouts::table::TableStrategy;
227 use vortex_layout::segments::TestSegments;
228 use vortex_layout::sequence::SequenceId;
229 use vortex_layout::sequence::SequentialArrayStreamExt;
230 use vortex_layout::session::LayoutSession;
231 use vortex_mask::Mask;
232 use vortex_session::VortexSession;
233
234 use super::*;
235
236 static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
237 VortexSession::empty()
238 .with::<ArraySession>()
239 .with::<LayoutSession>()
240 .with::<ScalarFnSession>()
241 .with::<RuntimeSession>()
242 });
243
244 fn test_file_stats(min: i32, max: i32) -> FileStatistics {
245 let mut stats = StatsSet::default();
246 stats.set(Stat::Min, Precision::exact(ScalarValue::from(min)));
247 stats.set(Stat::Max, Precision::exact(ScalarValue::from(max)));
248 FileStatistics::new(
249 Arc::from([stats]),
250 Arc::from([DType::Primitive(PType::I32, Nullability::NonNullable)]),
251 )
252 }
253
254 #[test]
255 fn pruning_when_filter_out_of_range() -> VortexResult<()> {
256 block_on(|handle| async {
257 let session = SESSION.clone().with_handle(handle);
258 let ctx = ArrayContext::empty();
259 let segments = Arc::new(TestSegments::default());
260 let (ptr, eof) = SequenceId::root().split();
261 let struct_array = StructArray::from_fields(
262 [("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
263 )?;
264 let strategy = TableStrategy::new(
265 Arc::new(FlatLayoutStrategy::default()),
266 Arc::new(FlatLayoutStrategy::default()),
267 );
268 let layout = strategy
269 .write_stream(
270 ctx,
271 Arc::<TestSegments>::clone(&segments),
272 struct_array.into_array().to_array_stream().sequenced(ptr),
273 eof,
274 &session,
275 )
276 .await?;
277
278 let child = layout.new_reader("".into(), segments, &SESSION)?;
279
280 let reader =
281 FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
282
283 let expr = gt(get_item("col", root()), lit(200i32));
285 let mask = Mask::new_true(5);
286 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
287 assert_eq!(result, Mask::new_false(5));
288
289 Ok(())
290 })
291 }
292
293 #[test]
294 fn no_pruning_when_filter_in_range() -> VortexResult<()> {
295 block_on(|handle| async {
296 let session = SESSION.clone().with_handle(handle);
297 let ctx = ArrayContext::empty();
298 let segments = Arc::new(TestSegments::default());
299 let (ptr, eof) = SequenceId::root().split();
300 let struct_array = StructArray::from_fields(
301 [("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
302 )?;
303 let strategy = TableStrategy::new(
304 Arc::new(FlatLayoutStrategy::default()),
305 Arc::new(FlatLayoutStrategy::default()),
306 );
307 let layout = strategy
308 .write_stream(
309 ctx,
310 Arc::<TestSegments>::clone(&segments),
311 struct_array.into_array().to_array_stream().sequenced(ptr),
312 eof,
313 &session,
314 )
315 .await?;
316
317 let child = layout.new_reader("".into(), segments, &SESSION)?;
318
319 let reader =
320 FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
321
322 let expr = gt(get_item("col", root()), lit(50i32));
324 let mask = Mask::new_true(5);
325 let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
326 assert_eq!(result, Mask::new_true(5));
328
329 Ok(())
330 })
331 }
332}