Skip to main content

datafusion_optimizer/
utils.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
18//! Utility functions leveraged by the query optimizer rules
19
20use std::collections::{BTreeSet, HashMap, HashSet};
21
22use crate::analyzer::type_coercion::TypeCoercionRewriter;
23use arrow::array::{Array, RecordBatch, new_null_array};
24use arrow::datatypes::{DataType, Field, Schema};
25use datafusion_common::TableReference;
26use datafusion_common::cast::as_boolean_array;
27use datafusion_common::tree_node::{TransformedResult, TreeNode, TreeNodeRecursion};
28use datafusion_common::{Column, DFSchema, Result, ScalarValue};
29use datafusion_expr::execution_props::ExecutionProps;
30use datafusion_expr::expr::{Exists, InSubquery, SetComparison};
31use datafusion_expr::expr_rewriter::replace_col;
32use datafusion_expr::physical_planning_context::PhysicalPlanningContext;
33use datafusion_expr::{ColumnarValue, Expr, logical_plan::LogicalPlan};
34use datafusion_physical_expr::create_physical_expr;
35use log::{debug, trace};
36use std::sync::Arc;
37
38/// Re-export of `NamesPreserver` for backwards compatibility,
39/// as it was initially placed here and then moved elsewhere.
40pub use datafusion_expr::expr_rewriter::NamePreserver;
41
42/// Invokes `f` with the index, within `schema`, of every column referenced by
43/// `expr` — including columns reached through a correlated subquery's outer
44/// references. Columns absent from `schema` are skipped.
45///
46/// A subquery's own plan is intentionally not traversed: its internal columns
47/// index into its own schema, not `schema`; only the outer (correlated) columns
48/// it references from `schema` are relevant. The comparison expression of an
49/// `IN`/set-comparison subquery is reached by the normal expression walk.
50///
51/// This is the shared primitive behind the top-down "which of a node's output
52/// columns does an ancestor still need" analyses, namely
53/// [`OptimizeProjections`](crate::optimize_projections::OptimizeProjections)
54/// and [`EliminateJoin`](crate::eliminate_join::EliminateJoin). The two keep
55/// their own required-index containers (an ordered set vs. a hash set), so this
56/// reports indices through a callback rather than populating a shared type.
57pub(crate) fn for_each_referenced_index(
58    expr: &Expr,
59    schema: &DFSchema,
60    mut f: impl FnMut(usize),
61) -> Result<()> {
62    visit_referenced_indices(expr, schema, &mut f)
63}
64
65fn visit_referenced_indices(
66    expr: &Expr,
67    schema: &DFSchema,
68    f: &mut dyn FnMut(usize),
69) -> Result<()> {
70    expr.apply(|expr| {
71        match expr {
72            Expr::Column(column) | Expr::OuterReferenceColumn(_, column) => {
73                if let Some(idx) = schema.maybe_index_of_column(column) {
74                    f(idx);
75                }
76            }
77            Expr::Exists(Exists { subquery, .. })
78            | Expr::InSubquery(InSubquery { subquery, .. })
79            | Expr::SetComparison(SetComparison { subquery, .. })
80            | Expr::ScalarSubquery(subquery) => {
81                for outer in &subquery.outer_ref_columns {
82                    visit_referenced_indices(outer, schema, f)?;
83                }
84            }
85            _ => {}
86        }
87        Ok(TreeNodeRecursion::Continue)
88    })?;
89    Ok(())
90}
91
92/// Returns true if `expr` contains all columns in `schema_cols`
93pub(crate) fn has_all_column_refs(
94    expr: &Expr,
95    schema_cols: &HashSet<ColumnReference>,
96) -> bool {
97    let column_refs = expr.column_refs();
98    // note can't use HashSet::intersect because of different types (owned vs References)
99    column_refs
100        .iter()
101        .filter(|c| {
102            schema_cols.contains(&ColumnReference::new(c.relation.as_ref(), c.name()))
103        })
104        .count()
105        == column_refs.len()
106}
107
108pub(crate) fn replace_qualified_name(
109    expr: Expr,
110    cols: &BTreeSet<Column>,
111    subquery_alias: &str,
112) -> Result<Expr> {
113    let alias_cols: Vec<Column> = cols
114        .iter()
115        .map(|col| Column::new(Some(subquery_alias), &col.name))
116        .collect();
117    let replace_map: HashMap<&Column, &Column> =
118        cols.iter().zip(alias_cols.iter()).collect();
119
120    replace_col(expr, &replace_map)
121}
122
123/// Column reference to avoid copying string around
124#[derive(PartialEq, Eq, Hash, Debug)]
125pub(crate) struct ColumnReference<'a> {
126    pub relation: Option<&'a TableReference>,
127    pub name: &'a str,
128}
129
130impl<'a> ColumnReference<'a> {
131    pub fn new(relation: Option<&'a TableReference>, name: &'a str) -> Self {
132        Self { relation, name }
133    }
134
135    pub fn new_unqualified(name: &'a str) -> Self {
136        Self {
137            relation: None,
138            name,
139        }
140    }
141}
142
143/// Returns references to all columns in the schema
144pub(crate) fn schema_columns<'a>(schema: &'a DFSchema) -> HashSet<ColumnReference<'a>> {
145    schema
146        .iter()
147        .flat_map(|(qualifier, field)| {
148            [
149                ColumnReference::new(qualifier, field.name()),
150                // we need to push down filter using unqualified column as well
151                ColumnReference::new_unqualified(field.name()),
152            ]
153        })
154        .collect::<HashSet<_>>()
155}
156
157/// Log the plan in debug/tracing mode after some part of the optimizer runs
158pub fn log_plan(description: &str, plan: &LogicalPlan) {
159    debug!("{description}:\n{}\n", plan.display_indent());
160    trace!("{description}::\n{}\n", plan.display_indent_schema());
161}
162
163/// Determine whether a predicate can restrict NULLs. e.g.
164/// `c0 > 8` return true;
165/// `c0 IS NULL` return false.
166pub fn is_restrict_null_predicate<'a>(
167    predicate: Expr,
168    join_cols_of_predicate: impl IntoIterator<Item = &'a Column>,
169) -> Result<bool> {
170    if matches!(predicate, Expr::Column(_)) {
171        return Ok(true);
172    }
173
174    // If result is single `true`, return false;
175    // If result is single `NULL` or `false`, return true;
176    Ok(
177        match evaluate_expr_with_null_column(predicate, join_cols_of_predicate)? {
178            ColumnarValue::Array(array) => {
179                if array.len() == 1 {
180                    let boolean_array = as_boolean_array(&array)?;
181                    boolean_array.is_null(0) || !boolean_array.value(0)
182                } else {
183                    false
184                }
185            }
186            ColumnarValue::Scalar(scalar) => matches!(
187                scalar,
188                ScalarValue::Boolean(None) | ScalarValue::Boolean(Some(false))
189            ),
190        },
191    )
192}
193
194/// Determines if an expression will always evaluate to null.
195/// `c0 + 8` return true
196/// `c0 IS NULL` return false
197/// `CASE WHEN c0 > 1 then 0 else 1` return false
198pub fn evaluates_to_null<'a>(
199    predicate: Expr,
200    null_columns: impl IntoIterator<Item = &'a Column>,
201) -> Result<bool> {
202    if matches!(predicate, Expr::Column(_)) {
203        return Ok(true);
204    }
205
206    Ok(
207        match evaluate_expr_with_null_column(predicate, null_columns)? {
208            ColumnarValue::Array(_) => false,
209            ColumnarValue::Scalar(scalar) => scalar.is_null(),
210        },
211    )
212}
213
214fn evaluate_expr_with_null_column<'a>(
215    predicate: Expr,
216    null_columns: impl IntoIterator<Item = &'a Column>,
217) -> Result<ColumnarValue> {
218    static DUMMY_COL_NAME: &str = "?";
219    let schema = Arc::new(Schema::new(vec![Field::new(
220        DUMMY_COL_NAME,
221        DataType::Null,
222        true,
223    )]));
224    let input_schema = DFSchema::try_from(Arc::clone(&schema))?;
225    let column = new_null_array(&DataType::Null, 1);
226    let input_batch = RecordBatch::try_new(schema, vec![column])?;
227    let execution_props = ExecutionProps::default();
228    let null_column = Column::from_name(DUMMY_COL_NAME);
229
230    let join_cols_to_replace = null_columns
231        .into_iter()
232        .map(|column| (column, &null_column))
233        .collect::<HashMap<_, _>>();
234
235    let replaced_predicate = replace_col(predicate, &join_cols_to_replace)?;
236    let coerced_predicate = coerce(replaced_predicate, &input_schema)?;
237    create_physical_expr(
238        &coerced_predicate,
239        &input_schema,
240        &execution_props,
241        &PhysicalPlanningContext::default(),
242    )?
243    .evaluate(&input_batch)
244}
245
246fn coerce(expr: Expr, schema: &DFSchema) -> Result<Expr> {
247    let mut expr_rewrite = TypeCoercionRewriter { schema };
248    expr.rewrite(&mut expr_rewrite).data()
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use datafusion_expr::{Operator, binary_expr, case, col, in_list, is_null, lit};
255
256    #[test]
257    fn expr_is_restrict_null_predicate() -> Result<()> {
258        let test_cases = vec![
259            // a
260            (col("a"), true),
261            // a IS NULL
262            (is_null(col("a")), false),
263            // a IS NOT NULL
264            (Expr::IsNotNull(Box::new(col("a"))), true),
265            // a = NULL
266            (
267                binary_expr(
268                    col("a"),
269                    Operator::Eq,
270                    Expr::Literal(ScalarValue::Null, None),
271                ),
272                true,
273            ),
274            // a > 8
275            (binary_expr(col("a"), Operator::Gt, lit(8i64)), true),
276            // a <= 8
277            (binary_expr(col("a"), Operator::LtEq, lit(8i32)), true),
278            // CASE a WHEN 1 THEN true WHEN 0 THEN false ELSE NULL END
279            (
280                case(col("a"))
281                    .when(lit(1i64), lit(true))
282                    .when(lit(0i64), lit(false))
283                    .otherwise(lit(ScalarValue::Null))?,
284                true,
285            ),
286            // CASE a WHEN 1 THEN true ELSE false END
287            (
288                case(col("a"))
289                    .when(lit(1i64), lit(true))
290                    .otherwise(lit(false))?,
291                true,
292            ),
293            // CASE a WHEN 0 THEN false ELSE true END
294            (
295                case(col("a"))
296                    .when(lit(0i64), lit(false))
297                    .otherwise(lit(true))?,
298                false,
299            ),
300            // (CASE a WHEN 0 THEN false ELSE true END) OR false
301            (
302                binary_expr(
303                    case(col("a"))
304                        .when(lit(0i64), lit(false))
305                        .otherwise(lit(true))?,
306                    Operator::Or,
307                    lit(false),
308                ),
309                false,
310            ),
311            // (CASE a WHEN 0 THEN true ELSE false END) OR false
312            (
313                binary_expr(
314                    case(col("a"))
315                        .when(lit(0i64), lit(true))
316                        .otherwise(lit(false))?,
317                    Operator::Or,
318                    lit(false),
319                ),
320                true,
321            ),
322            // a IN (1, 2, 3)
323            (
324                in_list(col("a"), vec![lit(1i64), lit(2i64), lit(3i64)], false),
325                true,
326            ),
327            // a NOT IN (1, 2, 3)
328            (
329                in_list(col("a"), vec![lit(1i64), lit(2i64), lit(3i64)], true),
330                true,
331            ),
332            // a IN (NULL)
333            (
334                in_list(
335                    col("a"),
336                    vec![Expr::Literal(ScalarValue::Null, None)],
337                    false,
338                ),
339                true,
340            ),
341            // a NOT IN (NULL)
342            (
343                in_list(col("a"), vec![Expr::Literal(ScalarValue::Null, None)], true),
344                true,
345            ),
346        ];
347
348        let column_a = Column::from_name("a");
349        for (predicate, expected) in test_cases {
350            let join_cols_of_predicate = std::iter::once(&column_a);
351            let actual =
352                is_restrict_null_predicate(predicate.clone(), join_cols_of_predicate)?;
353            assert_eq!(actual, expected, "{predicate}");
354        }
355
356        Ok(())
357    }
358}