use std::collections::BTreeSet;
use std::ops::Range;
use std::sync::Arc;
use vortex_array::Canonical;
use vortex_array::IntoArray;
use vortex_array::MaskFuture;
use vortex_array::VortexSessionExecute;
use vortex_array::arrays::NullArray;
use vortex_array::dtype::DType;
use vortex_array::dtype::FieldMask;
use vortex_array::dtype::FieldPath;
use vortex_array::dtype::StructFields;
use vortex_array::expr::Expression;
use vortex_array::expr::StatsCatalog;
use vortex_array::expr::lit;
use vortex_array::expr::stats::Stat;
use vortex_array::scalar::Scalar;
use vortex_array::scalar_fn::fns::literal::Literal;
use vortex_error::VortexResult;
use vortex_layout::ArrayFuture;
use vortex_layout::LayoutReader;
use vortex_layout::LayoutReaderRef;
use vortex_mask::Mask;
use vortex_session::VortexSession;
use vortex_utils::aliases::dash_map::DashMap;
use crate::FileStatistics;
pub struct FileStatsLayoutReader {
child: LayoutReaderRef,
file_stats: FileStatistics,
struct_fields: StructFields,
session: VortexSession,
prune_cache: DashMap<Expression, bool>,
}
impl FileStatsLayoutReader {
pub fn new(child: LayoutReaderRef, file_stats: FileStatistics, session: VortexSession) -> Self {
let struct_fields = child
.dtype()
.as_struct_fields_opt()
.cloned()
.unwrap_or_default();
Self {
child,
file_stats,
struct_fields,
session,
prune_cache: Default::default(),
}
}
fn evaluate_file_stats(&self, expr: &Expression) -> VortexResult<bool> {
let Some(pruning_expr) = expr.stat_falsification(self) else {
return Ok(false);
};
let simplified = pruning_expr.optimize_recursive(&DType::Null)?;
if let Some(result) = simplified.as_opt::<Literal>() {
return Ok(result.as_bool().value() == Some(true));
}
let pruning = NullArray::new(1).into_array().apply(&pruning_expr)?;
let mut ctx = self.session.create_execution_ctx();
let result = pruning
.execute::<Canonical>(&mut ctx)?
.into_bool()
.scalar_at(0)?;
Ok(result.as_bool().value() == Some(true))
}
}
impl StatsCatalog for FileStatsLayoutReader {
fn stats_ref(&self, field_path: &FieldPath, stat: Stat) -> Option<Expression> {
if field_path.parts().len() != 1 {
return None;
}
let field_name = field_path.parts()[0].as_name()?;
let field_idx = self.struct_fields.find(field_name)?;
let field_stats = self.file_stats.stats_sets().get(field_idx)?;
let stat_value = field_stats.get(stat)?.as_exact()?;
let field_dtype = self.struct_fields.field_by_index(field_idx)?;
let stat_scalar = Scalar::try_new(field_dtype, Some(stat_value)).ok()?;
Some(lit(stat_scalar))
}
}
impl LayoutReader for FileStatsLayoutReader {
fn name(&self) -> &Arc<str> {
self.child.name()
}
fn dtype(&self) -> &DType {
self.child.dtype()
}
fn row_count(&self) -> u64 {
self.child.row_count()
}
fn register_splits(
&self,
field_mask: &[FieldMask],
row_range: &Range<u64>,
splits: &mut BTreeSet<u64>,
) -> VortexResult<()> {
self.child.register_splits(field_mask, row_range, splits)
}
fn pruning_evaluation(
&self,
row_range: &Range<u64>,
expr: &Expression,
mask: Mask,
) -> VortexResult<MaskFuture> {
if let Some(pruned) = self.prune_cache.get(expr) {
if *pruned {
return Ok(MaskFuture::ready(Mask::new_false(mask.len())));
}
return self.child.pruning_evaluation(row_range, expr, mask);
}
let pruned = self.evaluate_file_stats(expr)?;
self.prune_cache.insert(expr.clone(), pruned);
if pruned {
Ok(MaskFuture::ready(Mask::new_false(mask.len())))
} else {
self.child.pruning_evaluation(row_range, expr, mask)
}
}
fn filter_evaluation(
&self,
row_range: &Range<u64>,
expr: &Expression,
mask: MaskFuture,
) -> VortexResult<MaskFuture> {
self.child.filter_evaluation(row_range, expr, mask)
}
fn projection_evaluation(
&self,
row_range: &Range<u64>,
expr: &Expression,
mask: MaskFuture,
) -> VortexResult<ArrayFuture> {
self.child.projection_evaluation(row_range, expr, mask)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::LazyLock;
use vortex_array::ArrayContext;
use vortex_array::IntoArray as _;
use vortex_array::arrays::StructArray;
use vortex_array::dtype::DType;
use vortex_array::dtype::Nullability;
use vortex_array::dtype::PType;
use vortex_array::expr::get_item;
use vortex_array::expr::gt;
use vortex_array::expr::lit;
use vortex_array::expr::root;
use vortex_array::expr::stats::Precision;
use vortex_array::expr::stats::Stat;
use vortex_array::scalar::ScalarValue;
use vortex_array::scalar_fn::session::ScalarFnSession;
use vortex_array::session::ArraySession;
use vortex_array::stats::StatsSet;
use vortex_buffer::buffer;
use vortex_error::VortexResult;
use vortex_io::runtime::single::block_on;
use vortex_io::session::RuntimeSession;
use vortex_layout::LayoutReader;
use vortex_layout::LayoutStrategy;
use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
use vortex_layout::layouts::table::TableStrategy;
use vortex_layout::segments::TestSegments;
use vortex_layout::sequence::SequenceId;
use vortex_layout::sequence::SequentialArrayStreamExt;
use vortex_layout::session::LayoutSession;
use vortex_mask::Mask;
use vortex_session::VortexSession;
use super::*;
static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
VortexSession::empty()
.with::<ArraySession>()
.with::<LayoutSession>()
.with::<ScalarFnSession>()
.with::<RuntimeSession>()
});
fn test_file_stats(min: i32, max: i32) -> FileStatistics {
let mut stats = StatsSet::default();
stats.set(Stat::Min, Precision::exact(ScalarValue::from(min)));
stats.set(Stat::Max, Precision::exact(ScalarValue::from(max)));
FileStatistics::new(
Arc::from([stats]),
Arc::from([DType::Primitive(PType::I32, Nullability::NonNullable)]),
)
}
#[test]
fn pruning_when_filter_out_of_range() -> VortexResult<()> {
block_on(|handle| async {
let ctx = ArrayContext::empty();
let segments = Arc::new(TestSegments::default());
let (ptr, eof) = SequenceId::root().split();
let struct_array = StructArray::from_fields(
[("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
)?;
let strategy = TableStrategy::new(
Arc::new(FlatLayoutStrategy::default()),
Arc::new(FlatLayoutStrategy::default()),
);
let layout = strategy
.write_stream(
ctx,
segments.clone(),
struct_array.into_array().to_array_stream().sequenced(ptr),
eof,
handle,
)
.await?;
let child = layout.new_reader("".into(), segments, &SESSION)?;
let reader =
FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
let expr = gt(get_item("col", root()), lit(200i32));
let mask = Mask::new_true(5);
let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
assert_eq!(result, Mask::new_false(5));
Ok(())
})
}
#[test]
fn no_pruning_when_filter_in_range() -> VortexResult<()> {
block_on(|handle| async {
let ctx = ArrayContext::empty();
let segments = Arc::new(TestSegments::default());
let (ptr, eof) = SequenceId::root().split();
let struct_array = StructArray::from_fields(
[("col", buffer![1i32, 2, 3, 4, 5].into_array())].as_slice(),
)?;
let strategy = TableStrategy::new(
Arc::new(FlatLayoutStrategy::default()),
Arc::new(FlatLayoutStrategy::default()),
);
let layout = strategy
.write_stream(
ctx,
segments.clone(),
struct_array.into_array().to_array_stream().sequenced(ptr),
eof,
handle,
)
.await?;
let child = layout.new_reader("".into(), segments, &SESSION)?;
let reader =
FileStatsLayoutReader::new(child, test_file_stats(0, 100), SESSION.clone());
let expr = gt(get_item("col", root()), lit(50i32));
let mask = Mask::new_true(5);
let result = reader.pruning_evaluation(&(0..5), &expr, mask)?.await?;
assert_eq!(result, Mask::new_true(5));
Ok(())
})
}
}