use std::collections::VecDeque;
use std::sync::Arc;
use arrow::array::RecordBatch;
use arrow::datatypes::SchemaRef;
use futures::StreamExt;
use futures::stream::BoxStream;
use log::debug;
use parquet::DecodeResult;
use parquet::arrow::ProjectionMask;
use parquet::arrow::arrow_reader::metrics::ArrowReaderMetrics;
use parquet::arrow::arrow_reader::{
ArrowReaderMetadata, ParquetRecordBatchReader, RowSelectionPolicy,
};
use parquet::arrow::async_reader::AsyncFileReader;
use parquet::arrow::push_decoder::{ParquetPushDecoder, ParquetPushDecoderBuilder};
use parquet::file::metadata::ParquetMetaData;
use datafusion_common::{DataFusionError, Result, internal_err};
use datafusion_physical_expr::expressions::DynamicFilterTracking;
use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
use datafusion_physical_plan::metrics::{BaselineMetrics, Count, Gauge};
use datafusion_pruning::{PruningPredicate, PruningPredicateBuilder};
use crate::access_plan::PreparedAccessPlan;
use crate::decoder_projection::DecoderProjection;
use crate::row_group_filter::RowGroupPruningStatistics;
pub(crate) struct DecoderBuilderConfig<'a> {
pub(crate) projection_mask: &'a ProjectionMask,
pub(crate) batch_size: usize,
pub(crate) arrow_reader_metrics: &'a ArrowReaderMetrics,
pub(crate) force_filter_selections: bool,
pub(crate) decoder_limit: Option<usize>,
}
impl DecoderBuilderConfig<'_> {
pub(crate) fn build(
&self,
prepared_access_plan: PreparedAccessPlan,
metadata: ArrowReaderMetadata,
) -> ParquetPushDecoderBuilder {
let mut builder = ParquetPushDecoderBuilder::new_with_metadata(metadata)
.with_projection(self.projection_mask.clone())
.with_batch_size(self.batch_size)
.with_metrics(self.arrow_reader_metrics.clone());
if self.force_filter_selections {
builder = builder.with_row_selection_policy(RowSelectionPolicy::Selectors);
}
if let Some(row_selection) = prepared_access_plan.row_selection {
builder = builder.with_row_selection(row_selection);
}
builder = builder.with_row_groups(prepared_access_plan.row_group_indexes);
if let Some(limit) = self.decoder_limit {
builder = builder.with_limit(limit);
}
builder
}
}
#[derive(Debug, Clone)]
pub(crate) struct RgPlanEntry {
pub(crate) rg_index: usize,
}
pub(crate) struct RowGroupPruner {
predicate: Arc<dyn PhysicalExpr>,
arrow_schema: SchemaRef,
parquet_metadata: Arc<ParquetMetaData>,
tracking: DynamicFilterTracking,
needs_initial_build: bool,
pruning_predicate: Option<Arc<PruningPredicate>>,
predicate_creation_errors: Count,
predicate_evaluation_errors: Count,
max_in_list_size: usize,
}
impl RowGroupPruner {
pub(crate) fn new(
predicate: Arc<dyn PhysicalExpr>,
arrow_schema: SchemaRef,
parquet_metadata: Arc<ParquetMetaData>,
predicate_creation_errors: Count,
predicate_evaluation_errors: Count,
max_in_list_size: usize,
) -> Self {
let tracking = DynamicFilterTracking::classify(&predicate);
Self {
predicate,
arrow_schema,
parquet_metadata,
tracking,
needs_initial_build: true,
pruning_predicate: None,
predicate_creation_errors,
predicate_evaluation_errors,
max_in_list_size,
}
}
pub(crate) fn should_prune(&mut self, row_group_indices: &[usize]) -> bool {
if row_group_indices.is_empty() {
return false;
}
let dynamic_changed = self
.tracking
.watcher()
.is_some_and(|tracker| tracker.changed());
if self.needs_initial_build || dynamic_changed {
self.pruning_predicate = PruningPredicateBuilder::new()
.with_file_schema(Arc::clone(&self.arrow_schema))
.with_error_counter(&self.predicate_creation_errors)
.with_max_in_list_size(self.max_in_list_size)
.build(Arc::clone(&self.predicate));
self.needs_initial_build = false;
}
let Some(pp) = self.pruning_predicate.as_ref() else {
return false;
};
let row_group_metadatas = row_group_indices
.iter()
.map(|&i| self.parquet_metadata.row_group(i))
.collect::<Vec<_>>();
let stats = RowGroupPruningStatistics {
parquet_schema: self.parquet_metadata.file_metadata().schema_descr(),
row_group_metadatas,
arrow_schema: self.arrow_schema.as_ref(),
missing_null_counts_as_zero: true,
};
match pp.prune(&stats) {
Ok(values) => values.iter().all(|&keep| !keep),
Err(e) => {
debug!(
"Ignoring error evaluating runtime row-group pruning predicate: {e}"
);
self.predicate_evaluation_errors.add(1);
false
}
}
}
}
pub(crate) struct PushDecoderStreamState {
pub(crate) decoder: Option<ParquetPushDecoder>,
pub(crate) active_reader: Option<ParquetRecordBatchReader>,
pub(crate) rg_plan: VecDeque<RgPlanEntry>,
pub(crate) reader: Box<dyn AsyncFileReader>,
pub(crate) decoder_projection: DecoderProjection,
pub(crate) arrow_reader_metrics: ArrowReaderMetrics,
pub(crate) predicate_cache_inner_records: Gauge,
pub(crate) predicate_cache_records: Gauge,
pub(crate) baseline_metrics: BaselineMetrics,
pub(crate) row_group_pruner: Option<RowGroupPruner>,
pub(crate) row_groups_pruned_dynamic: Count,
}
impl PushDecoderStreamState {
pub(crate) fn into_stream(self) -> BoxStream<'static, Result<RecordBatch>> {
futures::stream::unfold(self, |state| async move { state.transition().await })
.fuse()
.boxed()
}
async fn transition(mut self) -> Option<(Result<RecordBatch>, Self)> {
loop {
if let Some(reader) = self.active_reader.as_mut() {
match reader.next() {
Some(Ok(batch)) => {
let mut timer = self.baseline_metrics.elapsed_compute().timer();
self.copy_arrow_reader_metrics();
let result = self.project_batch(&batch);
timer.stop();
drop(timer);
return Some((result, self));
}
Some(Err(e)) => {
return Some((Err(DataFusionError::from(e)), self));
}
None => {
self.active_reader = None;
}
}
}
let at_boundary = self
.decoder
.as_ref()
.expect("decoder present")
.is_at_row_group_boundary();
if at_boundary
&& self.row_group_pruner.is_some()
&& let Err(e) = self.sync_rg_plan_to_decoder_frontier()
{
return Some((Err(e), self));
}
if at_boundary && !self.rg_plan.is_empty() {
let mut pruned_count = 0usize;
if let Some(pruner) = self.row_group_pruner.as_mut() {
let mut kept = VecDeque::with_capacity(self.rg_plan.len());
while let Some(entry) = self.rg_plan.pop_front() {
if pruner.should_prune(&[entry.rg_index]) {
pruned_count += 1;
self.row_groups_pruned_dynamic.add(1);
} else {
kept.push_back(entry);
}
}
self.rg_plan = kept;
}
if pruned_count > 0 {
if self.rg_plan.is_empty() {
return None;
}
let decoder = self.decoder.take().expect("decoder present");
let new_indices: Vec<usize> =
self.rg_plan.iter().map(|e| e.rg_index).collect();
let rebuilt = match decoder.into_builder() {
Ok(b) => b.with_row_groups(new_indices).build(),
Err(e) => Err(e),
};
match rebuilt {
Ok(d) => self.decoder = Some(d),
Err(e) => {
return Some((Err(DataFusionError::from(e)), self));
}
}
}
}
let decoder = self.decoder.as_mut().expect("decoder present");
match decoder.try_next_reader() {
Ok(DecodeResult::NeedsData(ranges)) => {
let data = self
.reader
.get_byte_ranges(ranges.clone())
.await
.map_err(DataFusionError::from);
match data {
Ok(data) => {
if let Err(e) = self
.decoder
.as_mut()
.expect("decoder present")
.push_ranges(ranges, data)
{
return Some((Err(DataFusionError::from(e)), self));
}
}
Err(e) => return Some((Err(e), self)),
}
}
Ok(DecodeResult::Data(reader)) => {
self.rg_plan.pop_front();
self.active_reader = Some(reader);
}
Ok(DecodeResult::Finished) => return None,
Err(e) => {
return Some((Err(DataFusionError::from(e)), self));
}
}
}
}
fn sync_rg_plan_to_decoder_frontier(&mut self) -> Result<()> {
match self
.decoder
.as_ref()
.expect("decoder present")
.peek_next_row_group()
.map_err(DataFusionError::from)?
{
Some(actual) => Self::advance_rg_plan_to(&mut self.rg_plan, actual)?,
None => self.rg_plan.clear(),
}
Ok(())
}
fn advance_rg_plan_to(
rg_plan: &mut VecDeque<RgPlanEntry>,
target: usize,
) -> Result<()> {
while let Some(front) = rg_plan.front() {
if front.rg_index == target {
return Ok(());
}
rg_plan.pop_front();
}
internal_err!(
"push decoder frontier RG {target} is not in rg_plan; \
decoder and plan have diverged"
)
}
fn copy_arrow_reader_metrics(&self) {
if let Some(v) = self.arrow_reader_metrics.records_read_from_inner() {
self.predicate_cache_inner_records.set(v);
}
if let Some(v) = self.arrow_reader_metrics.records_read_from_cache() {
self.predicate_cache_records.set(v);
}
}
fn project_batch(&self, batch: &RecordBatch) -> Result<RecordBatch> {
self.decoder_projection.map(batch)
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{Int64Array, RecordBatch};
use arrow::datatypes::{DataType, Field, Schema};
use bytes::Bytes;
use datafusion_common::ScalarValue;
use datafusion_expr::Operator;
use datafusion_physical_expr::expressions::{
BinaryExpr, Column, DynamicFilterPhysicalExpr, lit,
};
use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder};
use datafusion_pruning::MAX_IN_LIST_SIZE;
use parquet::arrow::ArrowWriter;
use parquet::file::metadata::ParquetMetaDataPushDecoder;
use parquet::file::properties::WriterProperties;
fn build_three_rg_file() -> (Arc<ParquetMetaData>, SchemaRef) {
let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)]));
let mut buf = Vec::new();
let props = WriterProperties::builder()
.set_max_row_group_row_count(Some(1000))
.build();
let mut writer =
ArrowWriter::try_new(&mut buf, Arc::clone(&schema), Some(props)).unwrap();
for rg in 0..3i64 {
let base = rg * 1000;
let vals: Vec<i64> = (base..base + 1000).collect();
let batch = RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(Int64Array::from(vals))],
)
.unwrap();
writer.write(&batch).unwrap();
writer.flush().unwrap();
}
writer.close().unwrap();
let file = Bytes::from(buf);
let len = file.len() as u64;
let mut md = ParquetMetaDataPushDecoder::try_new(len).unwrap();
#[expect(
clippy::single_range_in_vec_init,
reason = "we want a single range covering the whole file"
)]
let ranges = vec![0..len];
md.push_ranges(ranges, vec![file]).unwrap();
let DecodeResult::Data(meta) = md.try_decode().unwrap() else {
panic!("decoding metadata");
};
assert_eq!(meta.num_row_groups(), 3, "test fixture must have 3 RGs");
(Arc::new(meta), schema)
}
fn pruner_error_counters() -> (Count, Count) {
let metrics = ExecutionPlanMetricsSet::new();
let creation =
MetricBuilder::new(&metrics).counter("num_predicate_creation_errors", 0);
let evaluation =
MetricBuilder::new(&metrics).counter("predicate_evaluation_errors", 0);
(creation, evaluation)
}
fn gt_predicate(threshold: i64) -> Arc<dyn PhysicalExpr> {
Arc::new(BinaryExpr::new(
Arc::new(Column::new("v", 0)),
Operator::Gt,
lit(ScalarValue::Int64(Some(threshold))),
))
}
#[test]
fn row_group_pruner_skips_only_disqualified_row_groups() {
let (meta, schema) = build_three_rg_file();
let (creation, evaluation) = pruner_error_counters();
let mut pruner = RowGroupPruner::new(
gt_predicate(1500),
Arc::clone(&schema),
Arc::clone(&meta),
creation,
evaluation,
MAX_IN_LIST_SIZE,
);
assert!(pruner.should_prune(&[0]), "RG0 should be pruned");
assert!(!pruner.should_prune(&[1]), "RG1 must NOT be pruned");
assert!(!pruner.should_prune(&[2]), "RG2 must NOT be pruned");
assert!(
!pruner.should_prune(&[0, 1]),
"mixed run with a live RG must NOT be pruned"
);
assert!(!pruner.should_prune(&[]));
}
#[test]
fn row_group_pruner_tracks_dynamic_filter_updates() {
let (meta, schema) = build_three_rg_file();
let dynamic = Arc::new(DynamicFilterPhysicalExpr::new(
vec![Arc::new(Column::new("v", 0))],
gt_predicate(500),
));
let (creation, evaluation) = pruner_error_counters();
let mut pruner = RowGroupPruner::new(
Arc::clone(&dynamic) as Arc<dyn PhysicalExpr>,
Arc::clone(&schema),
Arc::clone(&meta),
creation,
evaluation,
MAX_IN_LIST_SIZE,
);
assert!(!pruner.should_prune(&[0]));
assert!(!pruner.should_prune(&[1]));
dynamic
.update(gt_predicate(2500))
.expect("update threshold");
assert!(
pruner.should_prune(&[0]),
"RG0 must be pruned after threshold tightens to 2500"
);
assert!(
pruner.should_prune(&[1]),
"RG1 must be pruned after threshold tightens to 2500"
);
assert!(
!pruner.should_prune(&[2]),
"RG2 (2000..3000) still straddles 2500"
);
}
#[test]
fn row_group_pruner_falls_back_to_conservative_when_predicate_has_no_bounds() {
let (meta, schema) = build_three_rg_file();
let (creation, evaluation) = pruner_error_counters();
let mut pruner = RowGroupPruner::new(
lit(true) as Arc<dyn PhysicalExpr>,
Arc::clone(&schema),
Arc::clone(&meta),
creation,
evaluation,
MAX_IN_LIST_SIZE,
);
assert!(!pruner.should_prune(&[0]));
assert!(!pruner.should_prune(&[1]));
assert!(!pruner.should_prune(&[2]));
}
#[test]
fn advance_rg_plan_to_pops_up_to_target() {
let mut plan: VecDeque<RgPlanEntry> = [0usize, 1, 2, 3]
.into_iter()
.map(|rg_index| RgPlanEntry { rg_index })
.collect();
PushDecoderStreamState::advance_rg_plan_to(&mut plan, 2).unwrap();
assert_eq!(
plan.iter().map(|e| e.rg_index).collect::<Vec<_>>(),
vec![2, 3],
"must pop the entries before `target` and stop at it",
);
}
#[test]
fn advance_rg_plan_to_errors_when_target_absent() {
let mut plan: VecDeque<RgPlanEntry> = [0usize, 1, 2]
.into_iter()
.map(|rg_index| RgPlanEntry { rg_index })
.collect();
let err = PushDecoderStreamState::advance_rg_plan_to(&mut plan, 5)
.expect_err("a target absent from the plan must be an internal error");
assert!(
err.to_string().contains("diverged"),
"expected a divergence internal error, got: {err}",
);
}
}