use std::sync::Arc;
use arrow_array::RecordBatch;
use arrow_schema::{ArrowError, DataType, Schema};
use parquet::arrow::arrow_reader::{ArrowPredicateFn, RowFilter};
use parquet::arrow::{ProjectionMask, parquet_to_arrow_schema};
use parquet::file::metadata::ParquetMetaData;
use crate::expr::filter::{Filter, filters_to_row_mask};
use crate::metadata::meta_field::MetaField;
use crate::statistics::StatisticsContainer;
use crate::storage::{RowFilterBuilder, RowGroupSelector};
use crate::table::file_pruner::FilePruner;
pub(crate) struct FilterPushdown {
pub row_filter: RowFilterBuilder,
pub row_groups: RowGroupSelector,
pub mor_pk_safe: bool,
}
pub(crate) fn filter_pushdown(
filters: &[Filter],
record_key_fields: &[String],
table_schema: &Schema,
merges: bool,
) -> Option<FilterPushdown> {
let is_key = |filter: &&Filter| {
filter.field == MetaField::RecordKey.as_ref() || record_key_fields.contains(&filter.field)
};
let filters: Vec<Filter> = if merges {
filters.iter().filter(is_key).cloned().collect()
} else {
filters.to_vec()
};
if filters.is_empty() {
return None;
}
let mor_pk_safe = filters.iter().all(|filter| is_key(&filter));
let table_types: Vec<(String, DataType)> = filters
.iter()
.map(|filter| {
let data_type = table_schema
.field_with_name(&filter.field)
.map(|f| f.data_type().clone())
.unwrap_or(DataType::Null);
(filter.field.clone(), data_type)
})
.collect();
let same_types = |file_schema: &Schema, table_types: &[(String, DataType)]| {
table_types.iter().all(|(name, data_type)| {
file_schema
.field_with_name(name)
.is_ok_and(|f| f.data_type() == data_type)
})
};
let for_rows = filters.clone();
let row_types = table_types.clone();
let row_filter: RowFilterBuilder = Arc::new(move |parquet_schema, read_schema| {
if !same_types(read_schema, &row_types) {
return None;
}
let root = parquet_schema.root_schema();
let mut columns: Vec<usize> = for_rows
.iter()
.map(|filter| {
root.get_fields()
.iter()
.position(|field| field.name() == filter.field)
})
.collect::<Option<_>>()?;
columns.sort_unstable();
columns.dedup();
let mask = ProjectionMask::roots(parquet_schema, columns);
let filters = for_rows.clone();
let predicate = ArrowPredicateFn::new(mask, move |batch: RecordBatch| {
filters_to_row_mask(&filters, &batch)
.map_err(|e| ArrowError::ComputeError(e.to_string()))
});
Some(RowFilter::new(vec![Box::new(predicate)]))
});
let for_groups: Vec<Filter> = filters
.iter()
.filter(|filter| {
!matches!(
table_schema
.field_with_name(&filter.field)
.map(|f| f.data_type()),
Ok(DataType::Float16 | DataType::Float32 | DataType::Float64)
)
})
.cloned()
.collect();
let group_types = table_types;
let row_groups: RowGroupSelector = Arc::new(move |metadata: &ParquetMetaData| {
let file = metadata.file_metadata();
let schema =
parquet_to_arrow_schema(file.schema_descr(), file.key_value_metadata()).ok()?;
if !same_types(&schema, &group_types) {
return None;
}
let pruner = FilePruner::new(&for_groups, &schema, &Schema::empty()).ok()?;
Some(
(0..metadata.num_row_groups())
.filter(|&index| {
pruner.should_include(&StatisticsContainer::from_row_group(
metadata.row_group(index),
&schema,
))
})
.collect(),
)
});
Some(FilterPushdown {
row_filter,
row_groups,
mor_pk_safe,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::filter::col;
use arrow_array::{Int64Array, StringArray};
use arrow_schema::{DataType, Field};
use bytes::Bytes;
use parquet::arrow::ArrowWriter;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use parquet::file::properties::WriterProperties;
fn schema() -> Schema {
Schema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("v", DataType::Int64, false),
])
}
fn parquet_file() -> Bytes {
let schema = Arc::new(schema());
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(StringArray::from(
(0..10).map(|i| format!("k{i:02}")).collect::<Vec<_>>(),
)),
Arc::new(Int64Array::from_iter_values(0..10)),
],
)
.unwrap();
let props = WriterProperties::builder()
.set_max_row_group_row_count(Some(2))
.build();
let mut out = Vec::new();
let mut writer = ArrowWriter::try_new(&mut out, schema, Some(props)).unwrap();
writer.write(&batch).unwrap();
writer.close().unwrap();
Bytes::from(out)
}
fn ids_through(pushdown: &FilterPushdown, file: Bytes) -> (Vec<usize>, Vec<String>) {
let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
let groups = (pushdown.row_groups)(builder.metadata()).unwrap();
let filter = (pushdown.row_filter)(builder.parquet_schema(), builder.schema()).unwrap();
let reader = builder
.with_row_groups(groups.clone())
.with_row_filter(filter)
.build()
.unwrap();
let ids = reader
.map(|b| b.unwrap())
.flat_map(|b| {
b.column(0)
.as_any()
.downcast_ref::<StringArray>()
.unwrap()
.iter()
.map(|v| v.unwrap().to_string())
.collect::<Vec<_>>()
})
.collect();
(groups, ids)
}
#[test]
fn test_pushdown_selects_row_groups_and_rows() {
let file = parquet_file();
let one = filter_pushdown(
&[col("id").eq("k05")],
&["id".to_string()],
&schema(),
false,
)
.unwrap();
assert!(one.mor_pk_safe);
assert_eq!(
ids_through(&one, file.clone()),
(vec![2], vec!["k05".to_string()])
);
let two =
filter_pushdown(&[col("id").in_list(["k01", "k08"])], &[], &schema(), false).unwrap();
assert!(!two.mor_pk_safe);
assert_eq!(
ids_through(&two, file.clone()),
(vec![0, 4], vec!["k01".to_string(), "k08".to_string()])
);
let both = filter_pushdown(
&[col("v").gte("6"), col("id").ne("k07")],
&[],
&schema(),
false,
)
.unwrap();
assert_eq!(
ids_through(&both, file.clone()),
(
vec![3, 4],
vec!["k06".to_string(), "k08".to_string(), "k09".to_string()]
)
);
let none = filter_pushdown(&[col("v").gt("100")], &[], &schema(), false).unwrap();
assert_eq!(ids_through(&none, file), (vec![], vec![]));
}
#[test]
fn test_a_merging_slice_pushes_only_record_key_filters() {
let file = parquet_file();
let keys = ["id".to_string()];
let mixed = [col("id").in_list(["k01", "k08"]), col("v").eq("8")];
let merging = filter_pushdown(&mixed, &keys, &schema(), true).unwrap();
assert!(merging.mor_pk_safe);
assert_eq!(
ids_through(&merging, file.clone()),
(vec![0, 4], vec!["k01".to_string(), "k08".to_string()])
);
let plain = filter_pushdown(&mixed, &keys, &schema(), false).unwrap();
assert!(!plain.mor_pk_safe);
assert_eq!(
ids_through(&plain, file),
(vec![4], vec!["k08".to_string()])
);
assert!(filter_pushdown(&[col("v").eq("8")], &keys, &schema(), true).is_none());
}
#[test]
fn test_pushdown_skips_a_narrower_file_type_and_float_statistics() {
let file = parquet_file();
let wide = Schema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("v", DataType::Decimal128(20, 0), false),
]);
let pushdown = filter_pushdown(&[col("v").gt("5")], &[], &wide, false).unwrap();
let builder = ParquetRecordBatchReaderBuilder::try_new(file.clone()).unwrap();
assert!((pushdown.row_filter)(builder.parquet_schema(), builder.schema()).is_none());
assert!((pushdown.row_groups)(builder.metadata()).is_none());
let floats = Schema::new(vec![
Field::new("id", DataType::Utf8, false),
Field::new("v", DataType::Float64, false),
]);
let pushdown = filter_pushdown(
&[col("v").gt("5"), col("id").eq("k01")],
&[],
&floats,
false,
)
.unwrap();
let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
assert!((pushdown.row_groups)(builder.metadata()).is_none());
}
#[test]
fn test_pushdown_skips_a_column_the_file_lacks() {
let file = parquet_file();
let pushdown = filter_pushdown(&[col("missing").eq("x")], &[], &schema(), false).unwrap();
let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
assert!((pushdown.row_filter)(builder.parquet_schema(), builder.schema()).is_none());
assert!((pushdown.row_groups)(builder.metadata()).is_none());
assert!(filter_pushdown(&[], &[], &schema(), false).is_none());
}
}