datafusion_physical_optimizer/
sanity_checker.rs1use std::sync::Arc;
25
26use datafusion_common::Result;
27use datafusion_physical_plan::ExecutionPlan;
28
29use datafusion_common::config::{ConfigOptions, OptimizerOptions};
30use datafusion_common::plan_err;
31use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
32use datafusion_physical_expr::intervals::utils::{check_support, is_datatype_supported};
33use datafusion_physical_plan::execution_plan::{
34 Boundedness, EmissionType, InvariantLevel,
35};
36use datafusion_physical_plan::joins::SymmetricHashJoinExec;
37use datafusion_physical_plan::{
38 ChildSatisfactionOptions, ExecutionPlanProperties, get_plan_string,
39};
40
41use crate::PhysicalOptimizerRule;
42use datafusion_physical_expr_common::sort_expr::format_physical_sort_requirement_list;
43use itertools::izip;
44
45#[derive(Default, Debug)]
51pub struct SanityCheckPlan {}
52
53impl SanityCheckPlan {
54 #[expect(missing_docs)]
55 pub fn new() -> Self {
56 Self {}
57 }
58}
59
60impl PhysicalOptimizerRule for SanityCheckPlan {
61 fn optimize(
62 &self,
63 plan: Arc<dyn ExecutionPlan>,
64 config: &ConfigOptions,
65 ) -> Result<Arc<dyn ExecutionPlan>> {
66 check_plan_sanity_recursive(&plan, &config.optimizer)?;
67 Ok(plan)
68 }
69
70 fn name(&self) -> &str {
71 "SanityCheckPlan"
72 }
73
74 fn schema_check(&self) -> bool {
75 true
76 }
77}
78
79#[cfg_attr(feature = "recursive_protection", recursive::recursive)]
81fn check_plan_sanity_recursive(
82 plan: &Arc<dyn ExecutionPlan>,
83 optimizer_options: &OptimizerOptions,
84) -> Result<TreeNodeRecursion> {
85 plan.apply_children(|child| check_plan_sanity_recursive(child, optimizer_options))?;
86 check_plan_sanity(plan, optimizer_options)?;
87 Ok(TreeNodeRecursion::Continue)
88}
89
90pub fn check_finiteness_requirements(
93 input: &dyn ExecutionPlan,
94 optimizer_options: &OptimizerOptions,
95) -> Result<()> {
96 if let Some(exec) = input.downcast_ref::<SymmetricHashJoinExec>()
97 && !(optimizer_options.allow_symmetric_joins_without_pruning
98 || (exec.check_if_order_information_available()? && is_prunable(exec)))
99 {
100 return plan_err!(
101 "Join operation cannot operate on a non-prunable stream without enabling \
102 the 'allow_symmetric_joins_without_pruning' configuration flag"
103 );
104 }
105
106 if matches!(
107 input.boundedness(),
108 Boundedness::Unbounded {
109 requires_infinite_memory: true
110 }
111 ) || (input.boundedness().is_unbounded()
112 && input.pipeline_behavior() == EmissionType::Final)
113 {
114 plan_err!(
115 "Cannot execute pipeline breaking queries, operator: {:?}",
116 input
117 )
118 } else {
119 Ok(())
120 }
121}
122
123fn is_prunable(join: &SymmetricHashJoinExec) -> bool {
131 join.filter().is_some_and(|filter| {
132 check_support(filter.expression(), &join.schema())
133 && filter
134 .schema()
135 .fields()
136 .iter()
137 .all(|f| is_datatype_supported(f.data_type()))
138 })
139}
140
141pub fn check_plan_sanity(
144 plan: &Arc<dyn ExecutionPlan>,
145 optimizer_options: &OptimizerOptions,
146) -> Result<()> {
147 check_finiteness_requirements(plan.as_ref(), optimizer_options)?;
148 let input_distributions = plan.input_distribution_requirements();
149
150 for ((idx, child), sort_req, dist_req) in izip!(
151 plan.children().into_iter().enumerate(),
152 plan.required_input_ordering(),
153 input_distributions.per_child_distributions(),
154 ) {
155 let child_eq_props = child.equivalence_properties();
156 if let Some(sort_req) = sort_req {
157 let sort_req = sort_req.into_single();
158 if !child_eq_props.ordering_satisfy_requirement(sort_req.clone())? {
159 let plan_str = get_plan_string(plan);
160 return plan_err!(
161 "Plan: {:?} does not satisfy order requirements: {}. Child-{} order: {}",
162 plan_str,
163 format_physical_sort_requirement_list(&sort_req),
164 idx,
165 child_eq_props.oeq_class()
166 );
167 }
168 }
169
170 if !input_distributions
171 .child_satisfaction(
172 idx,
173 child.as_ref(),
174 ChildSatisfactionOptions::new().with_allow_subset(true),
175 )?
176 .is_satisfied()
177 {
178 let plan_str = get_plan_string(plan);
179 return plan_err!(
180 "Plan: {:?} does not satisfy distribution requirements: {}. Child-{} output partitioning: {}",
181 plan_str,
182 dist_req,
183 idx,
184 child.output_partitioning()
185 );
186 }
187 }
188
189 plan.check_invariants(InvariantLevel::Executable)?;
190
191 Ok(())
192}
193
194