datafusion_expr/logical_plan/
invariants.rs1use datafusion_common::{
19 DFSchemaRef, Result, assert_or_internal_err, plan_err,
20 tree_node::{TreeNode, TreeNodeRecursion},
21};
22
23use crate::{
24 Aggregate, DmlStatement, Expr, Filter, Join, JoinType, LogicalPlan, Window, WriteOp,
25 expr::{Exists, InSubquery, SetComparison},
26 expr_rewriter::strip_outer_reference,
27 utils::{collect_subquery_cols, split_conjunction},
28};
29
30use super::Extension;
31
32#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Hash)]
33pub enum InvariantLevel {
34 Always,
37 Executable,
44}
45
46pub fn assert_always_invariants_at_current_node(plan: &LogicalPlan) -> Result<()> {
50 assert_unique_field_names(plan)?;
52
53 Ok(())
54}
55
56pub fn assert_executable_invariants(plan: &LogicalPlan) -> Result<()> {
59 assert_always_invariants_at_current_node(plan)?;
61 assert_valid_extension_nodes(plan, InvariantLevel::Always)?;
62
63 assert_valid_extension_nodes(plan, InvariantLevel::Executable)?;
65 assert_valid_semantic_plan(plan)?;
66 Ok(())
67}
68
69fn assert_valid_extension_nodes(plan: &LogicalPlan, check: InvariantLevel) -> Result<()> {
74 plan.apply_with_subqueries(|plan: &LogicalPlan| {
75 if let LogicalPlan::Extension(Extension { node }) = plan {
76 node.check_invariants(check)?;
77 }
78 plan.apply_expressions(|expr| {
79 expr.apply(|expr| {
81 match expr {
82 Expr::Exists(Exists { subquery, .. })
83 | Expr::InSubquery(InSubquery { subquery, .. })
84 | Expr::SetComparison(SetComparison { subquery, .. })
85 | Expr::ScalarSubquery(subquery) => {
86 assert_valid_extension_nodes(&subquery.subquery, check)?;
87 }
88 _ => {}
89 };
90 Ok(TreeNodeRecursion::Continue)
91 })
92 })
93 })
94 .map(|_| ())
95}
96
97fn assert_unique_field_names(plan: &LogicalPlan) -> Result<()> {
102 plan.schema().check_names()
103}
104
105fn assert_valid_semantic_plan(plan: &LogicalPlan) -> Result<()> {
107 assert_subqueries_are_valid(plan)?;
108
109 Ok(())
110}
111
112pub fn assert_expected_schema(schema: &DFSchemaRef, plan: &LogicalPlan) -> Result<()> {
115 let compatible = plan.schema().logically_equivalent_names_and_types(schema);
116
117 assert_or_internal_err!(
118 compatible,
119 "Failed due to a difference in schemas: original schema: {:?}, new schema: {:?}",
120 schema,
121 plan.schema()
122 );
123 Ok(())
124}
125
126fn assert_subqueries_are_valid(plan: &LogicalPlan) -> Result<()> {
130 plan.apply_with_subqueries(|plan: &LogicalPlan| {
131 plan.apply_expressions(|expr| {
132 expr.apply(|expr| {
134 match expr {
135 Expr::Exists(Exists { subquery, .. })
136 | Expr::InSubquery(InSubquery { subquery, .. })
137 | Expr::SetComparison(SetComparison { subquery, .. })
138 | Expr::ScalarSubquery(subquery) => {
139 check_subquery_expr(plan, &subquery.subquery, expr)?;
140 }
141 _ => {}
142 };
143 Ok(TreeNodeRecursion::Continue)
144 })
145 })
146 })
147 .map(|_| ())
148}
149
150pub fn check_subquery_expr(
158 outer_plan: &LogicalPlan,
159 inner_plan: &LogicalPlan,
160 expr: &Expr,
161) -> Result<()> {
162 assert_subqueries_are_valid(inner_plan)?;
163 if let Expr::ScalarSubquery(subquery) = expr {
164 if subquery.subquery.schema().fields().len() > 1 {
166 return plan_err!(
167 "Scalar subquery should only return one column, but found {}: {}",
168 subquery.subquery.schema().fields().len(),
169 subquery.subquery.schema().field_names().join(", ")
170 );
171 }
172 if !subquery.outer_ref_columns.is_empty() {
174 match strip_inner_query(inner_plan) {
175 LogicalPlan::Aggregate(agg) => {
176 check_aggregation_in_scalar_subquery(inner_plan, agg)
177 }
178 LogicalPlan::Filter(Filter { input, .. })
179 if matches!(input.as_ref(), LogicalPlan::Aggregate(_)) =>
180 {
181 if let LogicalPlan::Aggregate(agg) = input.as_ref() {
182 check_aggregation_in_scalar_subquery(inner_plan, agg)
183 } else {
184 Ok(())
185 }
186 }
187 _ => {
188 if inner_plan
189 .max_rows()
190 .filter(|max_row| *max_row <= 1)
191 .is_some()
192 {
193 Ok(())
194 } else {
195 plan_err!(
196 "Correlated scalar subquery must be aggregated to return at most one row"
197 )
198 }
199 }
200 }?;
201 match outer_plan {
202 LogicalPlan::Projection(_) | LogicalPlan::Filter(_) => Ok(()),
203 LogicalPlan::Aggregate(Aggregate {
204 group_expr,
205 aggr_expr,
206 ..
207 }) => {
208 if group_expr.contains(expr) && !aggr_expr.contains(expr) {
209 plan_err!(
211 "Correlated scalar subquery in the GROUP BY clause must \
212 also be in the aggregate expressions"
213 )
214 } else {
215 Ok(())
216 }
217 }
218 _ => plan_err!(
219 "Correlated scalar subquery can only be used in Projection, \
220 Filter, Aggregate plan nodes"
221 ),
222 }?;
223 }
224 check_correlations_in_subquery(inner_plan)
225 } else {
226 if let Expr::InSubquery(subquery) = expr {
227 if subquery.subquery.subquery.schema().fields().len() > 1 {
229 return plan_err!(
230 "InSubquery should only return one column, but found {}: {}",
231 subquery.subquery.subquery.schema().fields().len(),
232 subquery.subquery.subquery.schema().field_names().join(", ")
233 );
234 }
235 }
236 if let Expr::SetComparison(set_comparison) = expr
237 && set_comparison.subquery.subquery.schema().fields().len() > 1
238 {
239 return plan_err!(
240 "Set comparison subquery should only return one column, but found {}: {}",
241 set_comparison.subquery.subquery.schema().fields().len(),
242 set_comparison
243 .subquery
244 .subquery
245 .schema()
246 .field_names()
247 .join(", ")
248 );
249 }
250 match outer_plan {
251 LogicalPlan::Projection(_)
252 | LogicalPlan::Filter(_)
253 | LogicalPlan::TableScan(_)
254 | LogicalPlan::Window(_)
255 | LogicalPlan::Aggregate(_)
256 | LogicalPlan::Join(_)
257 | LogicalPlan::Dml(DmlStatement {
258 op: WriteOp::MergeInto(_),
259 ..
260 }) => Ok(()),
261 _ => plan_err!(
262 "In/Exist/SetComparison subquery can only be used in \
263 Projection, Filter, TableScan, Window functions, Aggregate and Join plan nodes, \
264 but was used in [{}]",
265 outer_plan.display()
266 ),
267 }?;
268 check_correlations_in_subquery(inner_plan)
269 }
270}
271
272fn check_correlations_in_subquery(inner_plan: &LogicalPlan) -> Result<()> {
274 check_inner_plan(inner_plan)
275}
276
277#[cfg_attr(feature = "recursive_protection", recursive::recursive)]
279fn check_inner_plan(inner_plan: &LogicalPlan) -> Result<()> {
280 match inner_plan {
282 LogicalPlan::Aggregate(_) => {
283 inner_plan.apply_children(|plan| {
284 check_inner_plan(plan)?;
285 Ok(TreeNodeRecursion::Continue)
286 })?;
287 Ok(())
288 }
289 LogicalPlan::Filter(Filter { input, .. }) => check_inner_plan(input),
290 LogicalPlan::Window(window) => {
291 check_mixed_out_refer_in_window(window)?;
292 inner_plan.apply_children(|plan| {
293 check_inner_plan(plan)?;
294 Ok(TreeNodeRecursion::Continue)
295 })?;
296 Ok(())
297 }
298 LogicalPlan::Projection(_)
299 | LogicalPlan::Distinct(_)
300 | LogicalPlan::Sort(_)
301 | LogicalPlan::Union(_)
302 | LogicalPlan::TableScan(_)
303 | LogicalPlan::EmptyRelation(_)
304 | LogicalPlan::Limit(_)
305 | LogicalPlan::Values(_)
306 | LogicalPlan::Subquery(_)
307 | LogicalPlan::SubqueryAlias(_)
308 | LogicalPlan::Unnest(_) => {
309 inner_plan.apply_children(|plan| {
310 check_inner_plan(plan)?;
311 Ok(TreeNodeRecursion::Continue)
312 })?;
313 Ok(())
314 }
315 LogicalPlan::Join(Join {
316 left,
317 right,
318 join_type,
319 ..
320 }) => match join_type {
321 JoinType::Inner => {
322 inner_plan.apply_children(|plan| {
323 check_inner_plan(plan)?;
324 Ok(TreeNodeRecursion::Continue)
325 })?;
326 Ok(())
327 }
328 JoinType::Left
329 | JoinType::LeftSemi
330 | JoinType::LeftAnti
331 | JoinType::LeftMark => {
332 check_inner_plan(left)?;
333 check_no_outer_references(right)
334 }
335 JoinType::Right
336 | JoinType::RightSemi
337 | JoinType::RightAnti
338 | JoinType::RightMark => {
339 check_no_outer_references(left)?;
340 check_inner_plan(right)
341 }
342 JoinType::Full => {
343 inner_plan.apply_children(|plan| {
344 check_no_outer_references(plan)?;
345 Ok(TreeNodeRecursion::Continue)
346 })?;
347 Ok(())
348 }
349 },
350 LogicalPlan::Extension(_) => Ok(()),
351 plan => check_no_outer_references(plan),
352 }
353}
354
355fn check_no_outer_references(inner_plan: &LogicalPlan) -> Result<()> {
356 if inner_plan.contains_outer_reference() {
357 plan_err!(
358 "Accessing outer reference columns is not allowed in the plan: {}",
359 inner_plan.display()
360 )
361 } else {
362 Ok(())
363 }
364}
365
366fn check_aggregation_in_scalar_subquery(
367 inner_plan: &LogicalPlan,
368 agg: &Aggregate,
369) -> Result<()> {
370 if agg.aggr_expr.is_empty() {
371 return plan_err!(
372 "Correlated scalar subquery must be aggregated to return at most one row"
373 );
374 }
375 if !agg.group_expr.is_empty() {
376 let correlated_exprs = get_correlated_expressions(inner_plan)?;
377 let inner_subquery_cols =
378 collect_subquery_cols(&correlated_exprs, agg.input.schema())?;
379 let mut group_columns = agg
380 .group_expr
381 .iter()
382 .map(|group| Ok(group.column_refs().into_iter().cloned().collect::<Vec<_>>()))
383 .collect::<Result<Vec<_>>>()?
384 .into_iter()
385 .flatten();
386
387 if !group_columns.all(|group| inner_subquery_cols.contains(&group)) {
388 return plan_err!(
390 "A GROUP BY clause in a scalar correlated subquery cannot contain non-correlated columns"
391 );
392 }
393 }
394 Ok(())
395}
396
397fn strip_inner_query(inner_plan: &LogicalPlan) -> &LogicalPlan {
398 match inner_plan {
399 LogicalPlan::Projection(projection) => {
400 strip_inner_query(projection.input.as_ref())
401 }
402 LogicalPlan::SubqueryAlias(alias) => strip_inner_query(alias.input.as_ref()),
403 other => other,
404 }
405}
406
407fn get_correlated_expressions(inner_plan: &LogicalPlan) -> Result<Vec<Expr>> {
408 let mut exprs = vec![];
409 inner_plan.apply_with_subqueries(|plan| {
410 if let LogicalPlan::Filter(Filter { predicate, .. }) = plan {
411 let (correlated, _): (Vec<_>, Vec<_>) = split_conjunction(predicate)
412 .into_iter()
413 .partition(|e| e.contains_outer());
414
415 for expr in correlated {
416 exprs.push(strip_outer_reference(expr.clone()));
417 }
418 }
419 Ok(TreeNodeRecursion::Continue)
420 })?;
421 Ok(exprs)
422}
423
424fn check_mixed_out_refer_in_window(window: &Window) -> Result<()> {
426 let mixed = window
427 .window_expr
428 .iter()
429 .any(|win_expr| win_expr.contains_outer() && win_expr.any_column_refs());
430 if mixed {
431 plan_err!(
432 "Window expressions should not contain a mixed of outer references and inner columns"
433 )
434 } else {
435 Ok(())
436 }
437}
438
439#[cfg(test)]
440mod test {
441 use std::cmp::Ordering;
442 use std::sync::Arc;
443
444 use crate::{Extension, UserDefinedLogicalNodeCore};
445 use datafusion_common::{DFSchema, DFSchemaRef};
446
447 use super::*;
448
449 #[derive(Debug, PartialEq, Eq, Hash)]
450 struct MockUserDefinedLogicalPlan {
451 empty_schema: DFSchemaRef,
452 }
453
454 impl PartialOrd for MockUserDefinedLogicalPlan {
455 fn partial_cmp(&self, _other: &Self) -> Option<Ordering> {
456 None
457 }
458 }
459
460 impl UserDefinedLogicalNodeCore for MockUserDefinedLogicalPlan {
461 fn name(&self) -> &str {
462 "MockUserDefinedLogicalPlan"
463 }
464
465 fn inputs(&self) -> Vec<&LogicalPlan> {
466 vec![]
467 }
468
469 fn schema(&self) -> &DFSchemaRef {
470 &self.empty_schema
471 }
472
473 fn expressions(&self) -> Vec<Expr> {
474 vec![]
475 }
476
477 fn fmt_for_explain(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
478 write!(f, "MockUserDefinedLogicalPlan")
479 }
480
481 fn with_exprs_and_inputs(
482 &self,
483 _exprs: Vec<Expr>,
484 _inputs: Vec<LogicalPlan>,
485 ) -> Result<Self> {
486 Ok(Self {
487 empty_schema: Arc::clone(&self.empty_schema),
488 })
489 }
490
491 fn supports_limit_pushdown(&self) -> bool {
492 false }
494 }
495
496 #[test]
497 fn wont_fail_extension_plan() {
498 let plan = LogicalPlan::Extension(Extension {
499 node: Arc::new(MockUserDefinedLogicalPlan {
500 empty_schema: DFSchemaRef::new(DFSchema::empty()),
501 }),
502 });
503
504 check_inner_plan(&plan).unwrap();
505 }
506}