Skip to main content

datafusion_expr/logical_plan/
invariants.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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    /// Invariants that are always true in DataFusion `LogicalPlan`s
35    /// such as the number of expected children and no duplicated output fields
36    Always,
37    /// Invariants that must hold true for the plan to be "executable"
38    /// such as the type and number of function arguments are correct and
39    /// that wildcards have been expanded
40    ///
41    /// To ensure a LogicalPlan satisfies the `Executable` invariants, run the
42    /// `Analyzer`
43    Executable,
44}
45
46/// Apply the [`InvariantLevel::Always`] check at the current plan node only.
47///
48/// This does not recurs to any child nodes.
49pub fn assert_always_invariants_at_current_node(plan: &LogicalPlan) -> Result<()> {
50    // Refer to <https://datafusion.apache.org/contributor-guide/specification/invariants.html#relation-name-tuples-in-logical-fields-and-logical-columns-are-unique>
51    assert_unique_field_names(plan)?;
52
53    Ok(())
54}
55
56/// Visit the plan nodes, and confirm the [`InvariantLevel::Executable`]
57/// as well as the less stringent [`InvariantLevel::Always`] checks.
58pub fn assert_executable_invariants(plan: &LogicalPlan) -> Result<()> {
59    // Always invariants
60    assert_always_invariants_at_current_node(plan)?;
61    assert_valid_extension_nodes(plan, InvariantLevel::Always)?;
62
63    // Executable invariants
64    assert_valid_extension_nodes(plan, InvariantLevel::Executable)?;
65    assert_valid_semantic_plan(plan)?;
66    Ok(())
67}
68
69/// Asserts that the query plan, and subplan, extension nodes have valid invariants.
70///
71/// Refer to [`UserDefinedLogicalNode::check_invariants`](super::UserDefinedLogicalNode)
72/// for more details of user-provided extension node invariants.
73fn 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            // recursively look for subqueries
80            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
97/// Returns an error if plan, and subplans, do not have unique fields.
98///
99/// This invariant is subject to change.
100/// refer: <https://github.com/apache/datafusion/issues/13525#issuecomment-2494046463>
101fn assert_unique_field_names(plan: &LogicalPlan) -> Result<()> {
102    plan.schema().check_names()
103}
104
105/// Returns an error if the plan is not semantically valid.
106fn assert_valid_semantic_plan(plan: &LogicalPlan) -> Result<()> {
107    assert_subqueries_are_valid(plan)?;
108
109    Ok(())
110}
111
112/// Returns an error if the plan does not have the expected schema.
113/// Ignores metadata and nullability.
114pub 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
126/// Asserts that the subqueries are structured properly with valid node placement.
127///
128/// Refer to [`check_subquery_expr`] for more details of the internal invariants.
129fn assert_subqueries_are_valid(plan: &LogicalPlan) -> Result<()> {
130    plan.apply_with_subqueries(|plan: &LogicalPlan| {
131        plan.apply_expressions(|expr| {
132            // recursively look for subqueries
133            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
150/// Do necessary check on subquery expressions and fail the invalid plan
151/// 1) Check whether the outer plan is in the allowed outer plans list to use subquery expressions,
152///    the allowed while list: [Projection, Filter, Window, Aggregate, Join].
153/// 2) Check whether the inner plan is in the allowed inner plans list to use correlated(outer) expressions.
154/// 3) Check and validate unsupported cases to use the correlated(outer) expressions inside the subquery(inner) plans/inner expressions.
155///    For example, we do not want to support to use correlated expressions as the Join conditions in the subquery plan when the Join
156///    is a Full Out Join
157pub 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        // Scalar subquery should only return one column
165        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        // Correlated scalar subquery must be aggregated to return at most one row
173        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                        // TODO revisit this validation logic
210                        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            // InSubquery should only return one column
228            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
272// Recursively check the unsupported outer references in the sub query plan.
273fn check_correlations_in_subquery(inner_plan: &LogicalPlan) -> Result<()> {
274    check_inner_plan(inner_plan)
275}
276
277// Recursively check the unsupported outer references in the sub query plan.
278#[cfg_attr(feature = "recursive_protection", recursive::recursive)]
279fn check_inner_plan(inner_plan: &LogicalPlan) -> Result<()> {
280    // We want to support as many operators as possible inside the correlated subquery
281    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            // Group BY columns must be a subset of columns in the correlated expressions
389            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
424/// Check whether the window expressions contain a mixture of out reference columns and inner columns
425fn 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 // Disallow limit push-down by default
493        }
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}