use std::{collections::BTreeMap, fmt};
use arrow::{
array::{Array, Date32Array, Int32Array, Int64Array, UInt32Array, UInt64Array},
datatypes::{DataType, Schema},
};
use crate::{
backend::AnalysisContext,
error::{QueryError, QueryErrorCode, Result},
expr::{BinaryOp, Expr, LiteralValue},
optimizer::{DimensionKeyFilter, KeyRange, parent_counts},
parquet_stats::{I64StatisticsRange as I64Range, row_group_i64_range_for_root_column},
plan::{JoinSide, NodeId, PlanNode, QueryPlan},
source::{LocalParquetSource, SourceColumnRef, SourceFooterFacts},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DynamicFilterId(pub(crate) usize);
impl DynamicFilterId {
pub const fn new(index: usize) -> Self {
Self(index)
}
pub const fn index(self) -> usize {
self.0
}
}
impl fmt::Display for DynamicFilterId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "df{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DynamicFilterKind {
Range,
SmallInList,
HashSetContains,
}
impl DynamicFilterKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Range => "range",
Self::SmallInList => "small_in_list",
Self::HashSetContains => "hash_set_contains",
}
}
}
impl fmt::Display for DynamicFilterKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DynamicFilterConsumerLowering {
ReaderPredicate,
MergedReaderPredicate,
PostDecodeOnly,
SkippedUnsupportedStaticPredicateMix,
}
impl DynamicFilterConsumerLowering {
pub const fn as_str(self) -> &'static str {
match self {
Self::ReaderPredicate => "reader_predicate",
Self::MergedReaderPredicate => "merged_reader_predicate",
Self::PostDecodeOnly => "post_decode_only",
Self::SkippedUnsupportedStaticPredicateMix => "skipped_unsupported_static_predicate_mix",
}
}
}
impl fmt::Display for DynamicFilterConsumerLowering {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DynamicFilterConsumerReductionStage {
SourceReader,
SourcePostDecode,
FilterPostDecode,
JoinProbePostDecode,
}
impl DynamicFilterConsumerReductionStage {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::SourceReader => "source_reader",
Self::SourcePostDecode => "source_post_decode",
Self::FilterPostDecode => "filter_post_decode",
Self::JoinProbePostDecode => "join_probe_post_decode",
}
}
}
impl fmt::Display for DynamicFilterConsumerReductionStage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DynamicFilterProducer {
HashJoinBuild {
join_node_id: NodeId,
build_side: JoinSide,
build_input: NodeId,
},
AggregateGroupKeys {
aggregate_node_id: NodeId,
},
}
impl DynamicFilterProducer {
pub const fn kind(&self) -> &'static str {
match self {
Self::HashJoinBuild { .. } => "hash_join_build",
Self::AggregateGroupKeys { .. } => "aggregate_group_keys",
}
}
pub const fn node_id(&self) -> NodeId {
match self {
Self::HashJoinBuild { join_node_id, .. } => *join_node_id,
Self::AggregateGroupKeys { aggregate_node_id } => *aggregate_node_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DynamicFilterConsumer {
LocalParquetSource {
source_node_id: NodeId,
},
HashJoinProbe {
join_node_id: NodeId,
probe_side: JoinSide,
},
Filter {
filter_node_id: NodeId,
},
}
impl DynamicFilterConsumer {
pub const fn kind(&self) -> &'static str {
match self {
Self::LocalParquetSource { .. } => "local_parquet_source",
Self::HashJoinProbe { .. } => "hash_join_probe",
Self::Filter { .. } => "filter",
}
}
pub const fn node_id(&self) -> NodeId {
match self {
Self::LocalParquetSource { source_node_id } => *source_node_id,
Self::HashJoinProbe { join_node_id, .. } => *join_node_id,
Self::Filter { filter_node_id } => *filter_node_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DynamicFilterRoute {
pub id: DynamicFilterId,
pub producer: DynamicFilterProducer,
pub consumer: DynamicFilterConsumer,
pub producer_columns: Vec<String>,
pub consumer_columns: Vec<String>,
pub(crate) filter_kinds: Vec<DynamicFilterKind>,
pub consumer_lowering: DynamicFilterConsumerLowering,
}
impl DynamicFilterRoute {
pub fn new(
id: DynamicFilterId,
producer: DynamicFilterProducer,
consumer: DynamicFilterConsumer,
producer_columns: Vec<String>,
consumer_columns: Vec<String>,
filter_kinds: Vec<DynamicFilterKind>,
consumer_lowering: DynamicFilterConsumerLowering,
) -> Self {
Self { id, producer, consumer, producer_columns, consumer_columns, filter_kinds, consumer_lowering }
}
pub const fn consumer_reduction_stage(&self) -> DynamicFilterConsumerReductionStage {
match (&self.consumer, self.consumer_lowering) {
(
DynamicFilterConsumer::LocalParquetSource { .. },
DynamicFilterConsumerLowering::ReaderPredicate | DynamicFilterConsumerLowering::MergedReaderPredicate,
) => DynamicFilterConsumerReductionStage::SourceReader,
(DynamicFilterConsumer::LocalParquetSource { .. }, _) => {
DynamicFilterConsumerReductionStage::SourcePostDecode
},
(DynamicFilterConsumer::Filter { .. }, _) => DynamicFilterConsumerReductionStage::FilterPostDecode,
(DynamicFilterConsumer::HashJoinProbe { .. }, _) => {
DynamicFilterConsumerReductionStage::JoinProbePostDecode
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DynamicFilter {
Empty,
RangeI64 {
min: i64,
max: i64,
},
ResidentKeysetI64 {
min: i64,
max: i64,
value_count: usize,
small_in_list: bool,
},
SmallInListI64 {
values: Vec<i64>,
min: i64,
max: i64,
},
HashSetContainsI64 {
values: Vec<i64>,
min: i64,
max: i64,
},
}
impl DynamicFilter {
pub fn from_i64_values(values: impl IntoIterator<Item = Option<i64>>, small_in_list_limit: usize) -> Self {
let mut values = values.into_iter().flatten().collect::<Vec<_>>();
if values.is_empty() {
return Self::Empty;
}
values.sort_unstable();
let min = values[0];
let max = *values.last().expect("non-empty values have a max");
values.dedup();
if values_form_dense_i64_range(&values, min, max) {
return Self::RangeI64 { min, max };
}
if values.len() <= small_in_list_limit {
Self::SmallInListI64 { values, min, max }
} else {
Self::HashSetContainsI64 { values, min, max }
}
}
pub const fn kind(&self) -> DynamicFilterKind {
match self {
Self::Empty | Self::RangeI64 { .. } => DynamicFilterKind::Range,
Self::SmallInListI64 { .. } => DynamicFilterKind::SmallInList,
Self::HashSetContainsI64 { .. } => DynamicFilterKind::HashSetContains,
Self::ResidentKeysetI64 { small_in_list: true, .. } => DynamicFilterKind::SmallInList,
Self::ResidentKeysetI64 { small_in_list: false, .. } => DynamicFilterKind::HashSetContains,
}
}
pub fn i64_range(&self) -> Option<(i64, i64)> {
match self {
Self::Empty => None,
Self::RangeI64 { min, max }
| Self::ResidentKeysetI64 { min, max, .. }
| Self::SmallInListI64 { min, max, .. }
| Self::HashSetContainsI64 { min, max, .. } => Some((*min, *max)),
}
}
pub fn value_count(&self) -> Option<usize> {
match self {
Self::Empty => Some(0),
Self::ResidentKeysetI64 { value_count, .. } => Some(*value_count),
Self::RangeI64 { .. } => None,
Self::SmallInListI64 { values, .. } | Self::HashSetContainsI64 { values, .. } => Some(values.len()),
}
}
}
fn values_form_dense_i64_range(values: &[i64], min: i64, max: i64) -> bool {
let Some(span) = i128::from(max).checked_sub(i128::from(min)).and_then(|span| span.checked_add(1)) else {
return false;
};
span == values.len() as i128
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DynamicFilterPayload {
Produced(DynamicFilter),
Skipped {
reason: &'static str,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DynamicFilterSet {
payloads: BTreeMap<DynamicFilterId, DynamicFilterPayload>,
}
impl DynamicFilterSet {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, id: DynamicFilterId, filter: DynamicFilter) {
self.payloads.insert(id, DynamicFilterPayload::Produced(filter));
}
pub fn insert_skipped(&mut self, id: DynamicFilterId, reason: &'static str) {
self.payloads.insert(id, DynamicFilterPayload::Skipped { reason });
}
pub fn is_ready(&self, id: DynamicFilterId) -> bool {
self.payloads.contains_key(&id)
}
pub fn produced(&self, id: DynamicFilterId) -> Option<&DynamicFilter> {
match self.payloads.get(&id) {
Some(DynamicFilterPayload::Produced(filter)) => Some(filter),
Some(DynamicFilterPayload::Skipped { .. }) | None => None,
}
}
pub fn merge_from(&mut self, other: &Self) {
for (id, payload) in &other.payloads {
self.payloads.insert(*id, payload.clone());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct DynamicFilterExecutionOptions {
#[doc(hidden)]
pub small_in_list_limit: usize,
#[doc(hidden)]
pub max_build_rows: usize,
#[doc(hidden)]
pub large_local_parquet_range_producer_rows: usize,
#[doc(hidden)]
pub dimension_sized_consumer_bytes: u64,
#[doc(hidden)]
pub tiny_local_parquet_consumer_row_groups: usize,
pub(crate) max_range_reader_predicate_selectivity_ppm: u64,
#[doc(hidden)]
pub max_deferred_small_in_list_reader_values: usize,
#[doc(hidden)]
pub max_unpruned_hash_set_post_decode_values: usize,
#[doc(hidden)]
pub min_unpruned_hash_set_post_decode_source_rows: u64,
}
impl DynamicFilterExecutionOptions {
const MAX_SELECTIVITY_PPM: u64 = 1_000_000;
pub(crate) const DEFAULT_SMALL_IN_LIST_LIMIT: usize = 1024;
pub(crate) const DEFAULT_MAX_BUILD_ROWS: usize = 1_000_000;
pub(crate) const DEFAULT_LARGE_LOCAL_PARQUET_RANGE_PRODUCER_ROWS: usize = 4 * 1024;
pub(crate) const DEFAULT_DIMENSION_SIZED_CONSUMER_BYTES: u64 = 256 * 1024 * 1024;
pub(crate) const DEFAULT_TINY_LOCAL_PARQUET_CONSUMER_ROW_GROUPS: usize = 32;
pub(crate) const DEFAULT_MAX_RANGE_READER_PREDICATE_SELECTIVITY_PPM: u64 = 950_000;
pub(crate) const DEFAULT_MAX_DEFERRED_SMALL_IN_LIST_READER_VALUES: usize = 128;
pub(crate) const DEFAULT_MAX_UNPRUNED_HASH_SET_POST_DECODE_VALUES: usize = 4096;
pub(crate) const DEFAULT_MIN_UNPRUNED_HASH_SET_POST_DECODE_SOURCE_ROWS: u64 = 1_000_000;
pub fn small_in_list_limit(&self) -> usize {
self.small_in_list_limit
}
pub fn max_build_rows(&self) -> usize {
self.max_build_rows
}
pub fn large_local_parquet_range_producer_rows(&self) -> usize {
self.large_local_parquet_range_producer_rows
}
pub fn dimension_sized_consumer_bytes(&self) -> u64 {
self.dimension_sized_consumer_bytes
}
pub fn tiny_local_parquet_consumer_row_groups(&self) -> usize {
self.tiny_local_parquet_consumer_row_groups
}
pub fn max_range_reader_predicate_selectivity_ppm(&self) -> u64 {
self.max_range_reader_predicate_selectivity_ppm
}
pub fn max_deferred_small_in_list_reader_values(&self) -> usize {
self.max_deferred_small_in_list_reader_values
}
pub fn max_unpruned_hash_set_post_decode_values(&self) -> usize {
self.max_unpruned_hash_set_post_decode_values
}
pub fn min_unpruned_hash_set_post_decode_source_rows(&self) -> u64 {
self.min_unpruned_hash_set_post_decode_source_rows
}
pub fn validate(&self) -> Result<()> {
if self.large_local_parquet_range_producer_rows != 0
&& self.max_build_rows != 0
&& self.large_local_parquet_range_producer_rows > self.max_build_rows
{
return Err(invalid_dynamic_filter_execution_option(
"large_local_parquet_range_producer_rows",
self.large_local_parquet_range_producer_rows,
"max_build_rows",
self.max_build_rows,
"range_producer_rows_exceeds_runtime_build_cap",
));
}
if self.max_range_reader_predicate_selectivity_ppm > Self::MAX_SELECTIVITY_PPM {
return Err(invalid_dynamic_filter_execution_option_max(
"max_range_reader_predicate_selectivity_ppm",
self.max_range_reader_predicate_selectivity_ppm,
Self::MAX_SELECTIVITY_PPM,
"selectivity_ppm_above_one_hundred_percent",
));
}
Ok(())
}
#[cfg(test)]
pub fn with_small_in_list_limit(mut self, limit: usize) -> Self {
self.small_in_list_limit = limit;
self
}
pub fn with_max_build_rows(mut self, rows: usize) -> Self {
self.max_build_rows = rows;
self
}
pub fn with_large_local_parquet_range_producer_rows(mut self, rows: usize) -> Self {
self.large_local_parquet_range_producer_rows = rows;
self
}
#[cfg(test)]
pub fn with_max_deferred_small_in_list_reader_values(mut self, values: usize) -> Self {
self.max_deferred_small_in_list_reader_values = values;
self
}
#[cfg(test)]
pub fn with_max_unpruned_hash_set_post_decode_values(mut self, values: usize) -> Self {
self.max_unpruned_hash_set_post_decode_values = values;
self
}
}
fn invalid_dynamic_filter_execution_option(
field: &'static str,
value: usize,
bound_field: &'static str,
bound_value: usize,
reason: &'static str,
) -> QueryError {
QueryError::new(
QueryErrorCode::InvalidConfiguration,
"query_engine.dynamic_filter.execution_options.validate",
"dynamic-filter execution option is inconsistent with another policy field",
)
.with_fact("field", field)
.with_fact("value", value)
.with_fact("bound_field", bound_field)
.with_fact("bound_value", bound_value)
.with_fact("reason", reason)
}
fn invalid_dynamic_filter_execution_option_max(
field: &'static str,
value: u64,
max_value: u64,
reason: &'static str,
) -> QueryError {
QueryError::new(
QueryErrorCode::InvalidConfiguration,
"query_engine.dynamic_filter.execution_options.validate",
"dynamic-filter execution option exceeds its valid maximum",
)
.with_fact("field", field)
.with_fact("value", value)
.with_fact("max_value", max_value)
.with_fact("reason", reason)
}
impl Default for DynamicFilterExecutionOptions {
fn default() -> Self {
Self {
small_in_list_limit: Self::DEFAULT_SMALL_IN_LIST_LIMIT,
max_build_rows: Self::DEFAULT_MAX_BUILD_ROWS,
large_local_parquet_range_producer_rows: Self::DEFAULT_LARGE_LOCAL_PARQUET_RANGE_PRODUCER_ROWS,
dimension_sized_consumer_bytes: Self::DEFAULT_DIMENSION_SIZED_CONSUMER_BYTES,
tiny_local_parquet_consumer_row_groups: Self::DEFAULT_TINY_LOCAL_PARQUET_CONSUMER_ROW_GROUPS,
max_range_reader_predicate_selectivity_ppm: Self::DEFAULT_MAX_RANGE_READER_PREDICATE_SELECTIVITY_PPM,
max_deferred_small_in_list_reader_values: Self::DEFAULT_MAX_DEFERRED_SMALL_IN_LIST_READER_VALUES,
max_unpruned_hash_set_post_decode_values: Self::DEFAULT_MAX_UNPRUNED_HASH_SET_POST_DECODE_VALUES,
min_unpruned_hash_set_post_decode_source_rows: Self::DEFAULT_MIN_UNPRUNED_HASH_SET_POST_DECODE_SOURCE_ROWS,
}
}
}
pub const LARGE_KEYSET_DIMENSION_SOURCE_SKIP_REASON: &str = "large_keyset_dimension_sized_local_parquet_consumer";
#[doc(hidden)]
pub fn dynamic_filter_consumer_source_feeds_downstream_work_fanout(plan: &QueryPlan, source_node_id: NodeId) -> bool {
let parent_counts = parent_counts(plan);
let mut current = source_node_id;
let mut saw_work_boundary = false;
loop {
let parent_count = parent_counts[current.index()];
if parent_count > 1 {
return saw_work_boundary;
}
if parent_count == 0 {
return false;
}
let Some(parent) = dynamic_filter_single_parent(plan, &parent_counts, current) else {
return false;
};
let Some(parent_node) = plan.node(parent) else {
return false;
};
if dynamic_filter_downstream_work_boundary(&parent_node.kind) {
saw_work_boundary = true;
}
current = parent;
}
}
fn dynamic_filter_single_parent(plan: &QueryPlan, parent_counts: &[usize], node_id: NodeId) -> Option<NodeId> {
if parent_counts[node_id.index()] != 1 {
return None;
}
plan.nodes().iter().find(|candidate| candidate.input_ids().contains(&node_id)).map(|candidate| candidate.id)
}
fn dynamic_filter_downstream_work_boundary(node: &PlanNode) -> bool {
matches!(
node,
PlanNode::Aggregate(_)
| PlanNode::Window(_)
| PlanNode::Join(_)
| PlanNode::Union(_)
| PlanNode::EdgeNormalize(_)
| PlanNode::GraphAlgorithm(_)
| PlanNode::VectorAlgorithm(_)
| PlanNode::MlAlgorithm(_)
)
}
pub fn dynamic_filter_i64_values(array: &dyn Array) -> Option<Vec<Option<i64>>> {
match array.data_type() {
DataType::Int32 => {
let array = array.as_any().downcast_ref::<Int32Array>()?;
Some((0..array.len()).map(|row| (!array.is_null(row)).then(|| i64::from(array.value(row)))).collect())
},
DataType::Date32 => {
let array = array.as_any().downcast_ref::<Date32Array>()?;
Some((0..array.len()).map(|row| (!array.is_null(row)).then(|| i64::from(array.value(row)))).collect())
},
DataType::Int64 => {
let array = array.as_any().downcast_ref::<Int64Array>()?;
Some((0..array.len()).map(|row| (!array.is_null(row)).then(|| array.value(row))).collect())
},
DataType::UInt32 => {
let array = array.as_any().downcast_ref::<UInt32Array>()?;
Some((0..array.len()).map(|row| (!array.is_null(row)).then(|| i64::from(array.value(row)))).collect())
},
DataType::UInt64 => {
let array = array.as_any().downcast_ref::<UInt64Array>()?;
(0..array.len())
.map(|row| if array.is_null(row) { Some(None) } else { i64::try_from(array.value(row)).ok().map(Some) })
.collect()
},
_ => None,
}
}
pub fn apply_dynamic_filters_to_local_parquet_source(
source: &LocalParquetSource,
routes: &[&DynamicFilterRoute],
filter_set: &DynamicFilterSet,
ctx: AnalysisContext<'_>,
footers: SourceFooterFacts<'_>,
) -> Result<LocalParquetSource> {
apply_dynamic_filters_to_local_parquet_source_with_reader_predicate_filter(
source,
routes,
filter_set,
|_, _| true,
ctx,
footers,
)
}
#[doc(hidden)]
pub fn apply_dynamic_filters_to_local_parquet_source_with_reader_predicate_filter(
source: &LocalParquetSource,
routes: &[&DynamicFilterRoute],
filter_set: &DynamicFilterSet,
mut should_merge_reader_predicate: impl FnMut(&DynamicFilterRoute, &DynamicFilter) -> bool,
ctx: AnalysisContext<'_>,
footers: SourceFooterFacts<'_>,
) -> Result<LocalParquetSource> {
let mut source = source.clone();
for route in routes {
let Some(filter) = filter_set.produced(route.id) else {
continue;
};
let Some(column) = route.consumer_columns.first() else {
continue;
};
let merge_reader_predicate = should_merge_reader_predicate(route, filter);
apply_dynamic_filter_to_source(&mut source, route, column, filter, merge_reader_predicate, ctx, footers)?;
}
Ok(source)
}
fn apply_dynamic_filter_to_source(
source: &mut LocalParquetSource,
route: &DynamicFilterRoute,
column: &str,
filter: &DynamicFilter,
merge_reader_predicate: bool,
ctx: AnalysisContext<'_>,
footers: SourceFooterFacts<'_>,
) -> Result<()> {
if matches!(filter, DynamicFilter::Empty) {
mark_source_row_groups_empty(source);
source.scan_limit = Some(0);
source.refresh_diagnostics();
return Ok(());
}
if let Some(key_filter) = dynamic_filter_row_group_key_filter(filter) {
prune_source_row_groups_by_key_filter(source, column, &key_filter, footers)?;
}
if merge_reader_predicate
&& matches!(
route.consumer_lowering,
DynamicFilterConsumerLowering::ReaderPredicate | DynamicFilterConsumerLowering::MergedReaderPredicate
)
&& let Some(predicate) = dynamic_filter_reader_predicate(source, column, filter, ctx)
&& source_reader_predicate_merge_is_supported(source, &predicate, ctx)
{
merge_source_reader_predicate(source, predicate);
}
Ok(())
}
fn dynamic_filter_row_group_key_filter(filter: &DynamicFilter) -> Option<DimensionKeyFilter> {
match filter {
DynamicFilter::Empty | DynamicFilter::ResidentKeysetI64 { .. } => None,
DynamicFilter::RangeI64 { min, max } => {
Some(DimensionKeyFilter::from_bounds(KeyRange { min: *min, max: *max }))
},
DynamicFilter::SmallInListI64 { values, .. } | DynamicFilter::HashSetContainsI64 { values, .. } => {
DimensionKeyFilter::from_keys(values)
},
}
}
fn prune_source_row_groups_by_key_filter(
source: &mut LocalParquetSource,
column: &str,
key_filter: &DimensionKeyFilter,
footers: SourceFooterFacts<'_>,
) -> Result<()> {
let SourceColumnRef::File(column_index) = source.source_column_ref(column).ok_or_else(|| {
QueryError::new(
QueryErrorCode::InvalidPlan,
"query_engine.dynamic_filter.source_pruning",
"dynamic-filter source column is not represented by the local parquet source",
)
.with_fact("column", column)
})?
else {
return Ok(());
};
let mut saw_statistics = false;
let mut selected_total = 0usize;
let mut retained_total = 0usize;
let mut selections = Vec::with_capacity(source.files.len());
for (file_index, file) in source.files.iter().enumerate() {
let Some(metadata) = footers.footer(file_index) else {
selections.push(file.row_groups.clone());
continue;
};
let selected = file.selected_or_all_row_groups(metadata.num_row_groups());
selected_total = selected_total.saturating_add(selected.len());
let mut retained = Vec::new();
for row_group_index in selected {
let row_group = metadata.row_group(row_group_index);
match row_group_i64_range_for_root_column(row_group, metadata.file_metadata().schema_descr(), column_index)
{
Some(row_group_range) => {
saw_statistics = true;
if key_filter.overlaps(KeyRange { min: row_group_range.min, max: row_group_range.max }) {
retained.push(row_group_index);
}
},
None => retained.push(row_group_index),
}
}
retained_total = retained_total.saturating_add(retained.len());
selections.push(Some(retained));
}
if !saw_statistics || retained_total >= selected_total {
return Ok(());
}
if retained_total == 0 {
for file in &mut source.files {
file.row_groups = Some(Vec::new());
}
source.scan_limit = Some(0);
} else {
for (file, selection) in source.files.iter_mut().zip(selections) {
if let Some(selection) = selection {
file.row_groups = Some(selection);
}
}
}
source.refresh_diagnostics();
Ok(())
}
fn mark_source_row_groups_empty(source: &mut LocalParquetSource) {
for file in &mut source.files {
file.row_groups = Some(Vec::new());
}
}
fn dynamic_filter_reader_predicate(
source: &LocalParquetSource,
column: &str,
filter: &DynamicFilter,
ctx: AnalysisContext<'_>,
) -> Option<Expr> {
if matches!(filter, DynamicFilter::HashSetContainsI64 { .. }) {
return None;
}
let predicate = dynamic_filter_predicate_for_schema(source.schema.as_ref(), column, filter)?;
source.reader_predicate_is_supported(&predicate, ctx).then_some(predicate)
}
#[doc(hidden)]
pub fn dynamic_filter_reader_predicate_should_merge(
source: &LocalParquetSource,
column: &str,
filter: &DynamicFilter,
options: DynamicFilterExecutionOptions,
ctx: AnalysisContext<'_>,
footers: SourceFooterFacts<'_>,
) -> Result<bool> {
let Some(predicate) = dynamic_filter_reader_predicate(source, column, filter, ctx) else {
return Ok(false);
};
if !source_reader_predicate_merge_is_supported(source, &predicate, ctx) {
return Ok(false);
}
let DynamicFilter::RangeI64 { min, max } = filter else {
return Ok(true);
};
let Some(source_range) = selected_source_i64_range(source, column, footers)? else {
return Ok(true);
};
let Some(overlap) = range_overlap_width(source_range, I64Range { min: *min, max: *max }) else {
return Ok(true);
};
let Some(source_width) = range_width(source_range) else {
return Ok(true);
};
Ok(overlap.saturating_mul(1_000_000)
<= source_width.saturating_mul(u128::from(options.max_range_reader_predicate_selectivity_ppm)))
}
fn selected_source_i64_range(
source: &LocalParquetSource,
column: &str,
footers: SourceFooterFacts<'_>,
) -> Result<Option<I64Range>> {
let SourceColumnRef::File(column_index) = source.source_column_ref(column).ok_or_else(|| {
QueryError::new(
QueryErrorCode::InvalidPlan,
"query_engine.dynamic_filter.reader_predicate_selectivity",
"dynamic-filter source column is not represented by the local parquet source",
)
.with_fact("column", column)
})?
else {
return Ok(None);
};
let mut selected_range = None;
for (file_index, file) in source.files.iter().enumerate() {
let Some(metadata) = footers.footer(file_index) else {
return Ok(None);
};
for row_group_index in file.selected_or_all_row_groups(metadata.num_row_groups()) {
let row_group = metadata.row_group(row_group_index);
let Some(row_group_range) =
row_group_i64_range_for_root_column(row_group, metadata.file_metadata().schema_descr(), column_index)
else {
return Ok(None);
};
selected_range = Some(match selected_range {
Some(existing) => merge_ranges(existing, row_group_range),
None => row_group_range,
});
}
}
Ok(selected_range)
}
fn merge_ranges(left: I64Range, right: I64Range) -> I64Range {
I64Range { min: left.min.min(right.min), max: left.max.max(right.max) }
}
fn range_width(range: I64Range) -> Option<u128> {
i128::from(range.max).checked_sub(i128::from(range.min))?.checked_add(1)?.try_into().ok()
}
fn range_overlap_width(left: I64Range, right: I64Range) -> Option<u128> {
let min = left.min.max(right.min);
let max = left.max.min(right.max);
(min <= max).then(|| range_width(I64Range { min, max })).flatten()
}
pub fn dynamic_filter_predicate_for_schema(schema: &Schema, column: &str, filter: &DynamicFilter) -> Option<Expr> {
let data_type = schema.field_with_name(column).ok()?.data_type();
let column_expr = Expr::column(column);
match filter {
DynamicFilter::ResidentKeysetI64 { .. } => None,
DynamicFilter::Empty => {
Some(Expr::binary(Expr::literal(LiteralValue::Int8(1)), BinaryOp::Eq, Expr::literal(LiteralValue::Int8(0))))
},
DynamicFilter::RangeI64 { min, max } => {
let lower = Expr::binary(
column_expr.clone(),
BinaryOp::GtEq,
Expr::literal(i64_literal_for_type(*min, data_type)?),
);
let upper =
Expr::binary(column_expr, BinaryOp::LtEq, Expr::literal(i64_literal_for_type(*max, data_type)?));
Some(Expr::binary(lower, BinaryOp::And, upper))
},
DynamicFilter::SmallInListI64 { values, .. } | DynamicFilter::HashSetContainsI64 { values, .. } => {
let values = values
.iter()
.map(|value| i64_literal_for_type(*value, data_type).map(Expr::literal))
.collect::<Option<Vec<_>>>()?;
Some(Expr::in_list(column_expr, values))
},
}
}
fn merge_source_reader_predicate(source: &mut LocalParquetSource, predicate: Expr) {
let predicate = match source.reader_predicate.take() {
Some(existing) => Expr::binary(existing, BinaryOp::And, predicate),
None => predicate,
};
source.reader_predicate = Some(predicate);
source.refresh_diagnostics();
}
fn source_reader_predicate_merge_is_supported(
source: &LocalParquetSource,
predicate: &Expr,
ctx: AnalysisContext<'_>,
) -> bool {
let predicate = match &source.reader_predicate {
Some(existing) => Expr::binary(existing.clone(), BinaryOp::And, predicate.clone()),
None => predicate.clone(),
};
source.reader_predicate_is_supported(&predicate, ctx)
}
fn i64_literal_for_type(value: i64, data_type: &DataType) -> Option<LiteralValue> {
match data_type {
DataType::Int32 => i32::try_from(value).ok().map(LiteralValue::Int32),
DataType::Date32 => i32::try_from(value).ok().map(LiteralValue::Date32),
DataType::Int64 => Some(LiteralValue::Int64(value)),
DataType::UInt32 => u32::try_from(value).ok().map(LiteralValue::UInt32),
DataType::UInt64 => u64::try_from(value).ok().map(LiteralValue::UInt64),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dynamic_filter_execution_options_accept_zero_threshold_overrides() {
DynamicFilterExecutionOptions::default().with_max_build_rows(0).validate().unwrap();
DynamicFilterExecutionOptions::default().with_large_local_parquet_range_producer_rows(0).validate().unwrap();
}
#[test]
fn dynamic_filter_i64_values_rejects_uint64_beyond_i64_max() {
let array = UInt64Array::from(vec![Some(1), None, Some(u64::MAX)]);
assert_eq!(dynamic_filter_i64_values(&array), None);
}
}