use std::sync::Arc;
use datafusion::{
common::{Result as DataFusionResult, config::OptimizerOptions},
datasource::{
physical_plan::{FileScanConfigBuilder, FileSource, ParquetSource},
source::DataSourceExec,
},
physical_optimizer::sanity_checker::check_plan_sanity,
physical_plan::{
ChildrenPropertiesMode, ExecutionPlan, ExecutionPlanProperties, ReplaceChildrenOptions,
coalesce_partitions::CoalescePartitionsExec, coop::CooperativeExec, filter::FilterExec,
repartition::RepartitionExec,
},
};
use query_planning::{
optimizer::{NativeOptimizerDiagnostics, NativeOptimizerOptions},
source::SourceDiagnostics,
};
use super::{
NativeCapabilityContractDiagnostics,
attempt::{NativeOptimizationAttempt, NativeReplacementSink, replace_lowered_root_or_keep_plan_for_sink},
plan_contains_gpu_execution,
};
use crate::{
capability::CapabilityReason,
native::{lower_physical_plan_to_native, scan_predicate_exactly_matches_filter},
native_exec::AlgeonNativeExecutionConfig,
physical_plan::DataFusionCpuBoundaryExec,
report::{CandidateOutcome, NotSupportedDetail, PlanBlocker, PlanningReport},
};
pub(super) struct FragmentSearch<'a> {
pub(super) native_execution_config: &'a AlgeonNativeExecutionConfig,
pub(super) native_optimizer_options: &'a NativeOptimizerOptions,
pub(super) datafusion_cpu_allowed: bool,
pub(super) optimizer_options: &'a OptimizerOptions,
}
#[derive(Default)]
struct FragmentEvidence {
report: PlanningReport,
native_optimizer: NativeOptimizerDiagnostics,
source_diagnostics: Vec<SourceDiagnostics>,
capability_contract: NativeCapabilityContractDiagnostics,
}
impl FragmentEvidence {
fn of_attempt(attempt: NativeOptimizationAttempt) -> Self {
Self {
report: attempt.report,
native_optimizer: attempt.native_optimizer,
source_diagnostics: attempt.source_diagnostics,
capability_contract: attempt.capability_contract,
}
}
fn append(&mut self, other: Self) {
self.report.append(other.report);
self.native_optimizer.merge(other.native_optimizer);
self.source_diagnostics.extend(other.source_diagnostics);
self.capability_contract.merge(other.capability_contract);
}
fn append_abandoned(&mut self, abandoned: Self, parent: &str) {
self.report.append(PlanningReport::from_outcomes(abandoned.report.outcomes().iter().map(
|outcome| match outcome {
CandidateOutcome::Selected { candidate } => CandidateOutcome::NotSupported {
candidate: candidate.clone(),
capability_reason: CapabilityReason::NativeFragmentParentContractUnsupported,
detail: NotSupportedDetail {
blocker: Some(Box::new(PlanBlocker::ParentOperator(parent.to_string()))),
..NotSupportedDetail::default()
},
},
CandidateOutcome::NotSupported { .. } | CandidateOutcome::NotSelectedByCost { .. } => outcome.clone(),
},
)));
}
}
struct Replacement {
plan: Arc<dyn ExecutionPlan>,
evidence: FragmentEvidence,
whole: bool,
}
impl FragmentSearch<'_> {
pub(super) fn below_root(
&self,
root_attempt: NativeOptimizationAttempt,
) -> DataFusionResult<NativeOptimizationAttempt> {
if root_attempt.rewritten || root_attempt.report.not_selected_by_cost_count() > 0 {
return Ok(root_attempt);
}
let mut evidence = FragmentEvidence::default();
let plan = self.below(&root_attempt.plan, None, &mut evidence)?;
let mut attempt = root_attempt;
attempt.rewritten = !Arc::ptr_eq(&plan, &attempt.plan);
attempt.plan = plan;
attempt.report.append(evidence.report);
attempt.native_optimizer.merge(evidence.native_optimizer);
attempt.source_diagnostics.extend(evidence.source_diagnostics);
attempt.capability_contract.merge(evidence.capability_contract);
Ok(attempt)
}
fn below(
&self,
plan: &Arc<dyn ExecutionPlan>,
filter_above: Option<&FilterExec>,
evidence: &mut FragmentEvidence,
) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
if plan.downcast_ref::<DataFusionCpuBoundaryExec>().is_some() {
return Ok(Arc::clone(plan));
}
let filter_above_children =
plan.downcast_ref::<FilterExec>().or_else(|| filter_above.filter(|_| passes_rows_through(plan.as_ref())));
let children = plan.children().into_iter().cloned().collect::<Vec<_>>();
let mut current = Arc::clone(plan);
for (index, child) in children.iter().enumerate() {
let mut next = self.replacement(child, filter_above_children, evidence)?;
while let Some(replacement) = next.take() {
if let Some(rebuilt) = self.keep(¤t, index, child, &replacement.plan)? {
current = rebuilt;
evidence.append(replacement.evidence);
break;
}
evidence.append_abandoned(replacement.evidence, current.name());
if replacement.whole {
next = self.rebuilt_over_fragments(child, filter_above_children, evidence)?;
}
}
}
Ok(current)
}
fn replacement(
&self,
child: &Arc<dyn ExecutionPlan>,
filter_above: Option<&FilterExec>,
evidence: &mut FragmentEvidence,
) -> DataFusionResult<Option<Replacement>> {
if plan_contains_gpu_execution(child.as_ref()) {
return Ok(None);
}
let input =
filter_above.and_then(|filter| without_pruning_copy(filter, child)).unwrap_or_else(|| Arc::clone(child));
let lowering =
lower_physical_plan_to_native(Arc::clone(&input), &self.native_execution_config.source_read_strategy())?;
let attempt = replace_lowered_root_or_keep_plan_for_sink(
input,
lowering,
self.native_execution_config,
self.native_optimizer_options,
self.datafusion_cpu_allowed,
NativeReplacementSink::HostArrow,
)?;
if attempt.rewritten {
return Ok(Some(Replacement {
plan: Arc::clone(&attempt.plan),
evidence: FragmentEvidence::of_attempt(attempt),
whole: true,
}));
}
if attempt.report.not_selected_by_cost_count() > 0 {
evidence.append(FragmentEvidence::of_attempt(attempt));
return Ok(None);
}
self.rebuilt_over_fragments(child, filter_above, evidence)
}
fn rebuilt_over_fragments(
&self,
child: &Arc<dyn ExecutionPlan>,
filter_above: Option<&FilterExec>,
evidence: &mut FragmentEvidence,
) -> DataFusionResult<Option<Replacement>> {
let mut below = FragmentEvidence::default();
let plan = self.below(child, filter_above, &mut below)?;
if Arc::ptr_eq(&plan, child) {
evidence.append(below);
return Ok(None);
}
Ok(Some(Replacement { plan, evidence: below, whole: false }))
}
fn keep(
&self,
parent: &Arc<dyn ExecutionPlan>,
index: usize,
child: &Arc<dyn ExecutionPlan>,
replacement: &Arc<dyn ExecutionPlan>,
) -> DataFusionResult<Option<Arc<dyn ExecutionPlan>>> {
if replacement.schema() != child.schema() || replacement.boundedness() != child.boundedness() {
return Ok(None);
}
let mut children = parent.children().into_iter().cloned().collect::<Vec<_>>();
children[index] = Arc::clone(replacement);
let rebuilt = Arc::clone(parent)
.replace_children(children, ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute))?;
Ok(check_plan_sanity(&rebuilt, self.optimizer_options).is_ok().then_some(rebuilt))
}
}
fn passes_rows_through(plan: &dyn ExecutionPlan) -> bool {
plan.downcast_ref::<RepartitionExec>().is_some()
|| plan.downcast_ref::<CoalescePartitionsExec>().is_some_and(|coalesce| coalesce.fetch().is_none())
|| plan.downcast_ref::<CooperativeExec>().is_some()
}
fn without_pruning_copy(filter: &FilterExec, plan: &Arc<dyn ExecutionPlan>) -> Option<Arc<dyn ExecutionPlan>> {
if !passes_rows_through(plan.as_ref()) {
return scan_without_filter_predicate(filter, plan);
}
let input = without_pruning_copy(filter, plan.children().first()?)?;
Arc::clone(plan).replace_children(vec![input], ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute)).ok()
}
fn scan_without_filter_predicate(
filter: &FilterExec,
child: &Arc<dyn ExecutionPlan>,
) -> Option<Arc<dyn ExecutionPlan>> {
let (file_scan, parquet_source) =
child.downcast_ref::<DataSourceExec>()?.downcast_to_file_source::<ParquetSource>()?;
if !scan_predicate_exactly_matches_filter(parquet_source, Some(filter)) {
return None;
}
let mut source = ParquetSource::new(parquet_source.table_schema().clone())
.with_table_parquet_options(parquet_source.table_parquet_options().clone());
if let Some(factory) = parquet_source.parquet_file_reader_factory() {
source = source.with_parquet_file_reader_factory(Arc::clone(factory));
}
let source = match parquet_source.projection() {
Some(projection) => source.try_pushdown_projection(projection).ok().flatten()?,
None => Arc::new(source) as Arc<dyn FileSource>,
};
Some(DataSourceExec::from_data_source(FileScanConfigBuilder::from(file_scan.clone()).with_source(source).build()))
}