Skip to main content

datafusion_expr/expr_rewriter/
mod.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//! Expression rewriter
19
20use std::collections::HashMap;
21use std::collections::HashSet;
22use std::fmt::Debug;
23use std::sync::Arc;
24
25use crate::expr::{Alias, Sort, Unnest};
26use crate::logical_plan::Projection;
27use crate::{Expr, ExprSchemable, LogicalPlan, LogicalPlanBuilder};
28
29use datafusion_common::TableReference;
30use datafusion_common::config::ConfigOptions;
31use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode};
32use datafusion_common::{Column, DFSchema, Result};
33
34mod guarantees;
35pub use guarantees::GuaranteeRewriter;
36pub use guarantees::rewrite_with_guarantees;
37pub use guarantees::rewrite_with_guarantees_map;
38mod order_by;
39
40pub use order_by::rewrite_sort_cols_by_aggs;
41
42/// Trait for rewriting [`Expr`]s into function calls.
43///
44/// This trait is used with `FunctionRegistry::register_function_rewrite` to
45/// to evaluating `Expr`s using functions that may not be built in to DataFusion
46///
47/// For example, concatenating arrays `a || b` is represented as
48/// `Operator::ArrowAt`, but can be implemented by calling a function
49/// `array_concat` from the `functions-nested` crate.
50// This is not used in datafusion internally, but it is still helpful for downstream project so don't remove it.
51pub trait FunctionRewrite: Debug {
52    /// Return a human readable name for this rewrite
53    fn name(&self) -> &str;
54
55    /// Potentially rewrite `expr` to some other expression
56    ///
57    /// Note that recursion is handled by the caller -- this method should only
58    /// handle `expr`, not recurse to its children.
59    fn rewrite(
60        &self,
61        expr: Expr,
62        schema: &DFSchema,
63        config: &ConfigOptions,
64    ) -> Result<Transformed<Expr>>;
65}
66
67/// Recursively call `LogicalPlanBuilder::normalize` on all [`Column`] expressions
68/// in the `expr` expression tree.
69pub fn normalize_col(expr: Expr, plan: &LogicalPlan) -> Result<Expr> {
70    expr.transform(|expr| {
71        Ok({
72            if let Expr::Column(c) = expr {
73                let col = LogicalPlanBuilder::normalize(plan, c)?;
74                Transformed::yes(Expr::Column(col))
75            } else {
76                Transformed::no(expr)
77            }
78        })
79    })
80    .data()
81}
82
83/// See [`Column::normalize_with_schemas_and_ambiguity_check`] for usage
84pub fn normalize_col_with_schemas_and_ambiguity_check(
85    expr: Expr,
86    schemas: &[&[&DFSchema]],
87    using_columns: &[HashSet<Column>],
88) -> Result<Expr> {
89    // Normalize column inside Unnest
90    if let Expr::Unnest(Unnest { expr, outer }) = expr {
91        let e = normalize_col_with_schemas_and_ambiguity_check(
92            expr.as_ref().clone(),
93            schemas,
94            using_columns,
95        )?;
96        return Ok(Expr::Unnest(Unnest {
97            expr: Box::new(e),
98            outer,
99        }));
100    }
101
102    expr.transform(|expr| {
103        Ok({
104            if let Expr::Column(c) = expr {
105                let col =
106                    c.normalize_with_schemas_and_ambiguity_check(schemas, using_columns)?;
107                Transformed::yes(Expr::Column(col))
108            } else {
109                Transformed::no(expr)
110            }
111        })
112    })
113    .data()
114}
115
116/// Recursively normalize all [`Column`] expressions in a list of expression trees
117pub fn normalize_cols(
118    exprs: impl IntoIterator<Item = impl Into<Expr>>,
119    plan: &LogicalPlan,
120) -> Result<Vec<Expr>> {
121    exprs
122        .into_iter()
123        .map(|e| normalize_col(e.into(), plan))
124        .collect()
125}
126
127pub fn normalize_sorts(
128    sorts: impl IntoIterator<Item = impl Into<Sort>>,
129    plan: &LogicalPlan,
130) -> Result<Vec<Sort>> {
131    sorts
132        .into_iter()
133        .map(|e| {
134            let sort = e.into();
135            normalize_col(sort.expr, plan)
136                .map(|expr| Sort::new(expr, sort.asc, sort.nulls_first))
137        })
138        .collect()
139}
140
141/// Recursively replace all [`Column`] expressions in a given expression tree with
142/// `Column` expressions provided by the hash map argument.
143pub fn replace_col(expr: Expr, replace_map: &HashMap<&Column, &Column>) -> Result<Expr> {
144    expr.transform(|expr| {
145        Ok({
146            if let Expr::Column(c) = &expr {
147                match replace_map.get(c) {
148                    Some(new_c) => Transformed::yes(Expr::Column((*new_c).to_owned())),
149                    None => Transformed::no(expr),
150                }
151            } else {
152                Transformed::no(expr)
153            }
154        })
155    })
156    .data()
157}
158
159/// Recursively 'unnormalize' (remove all qualifiers) from an
160/// expression tree.
161///
162/// For example, if there were expressions like `foo.bar` this would
163/// rewrite it to just `bar`.
164pub fn unnormalize_col(expr: Expr) -> Expr {
165    expr.transform(|expr| {
166        Ok({
167            if let Expr::Column(c) = expr {
168                let col = Column::new_unqualified(c.name);
169                Transformed::yes(Expr::Column(col))
170            } else {
171                Transformed::no(expr)
172            }
173        })
174    })
175    .data()
176    .expect("Unnormalize is infallible")
177}
178
179/// Create a Column from the Scalar Expr
180pub fn create_col_from_scalar_expr(
181    scalar_expr: &Expr,
182    subqry_alias: String,
183) -> Result<Column> {
184    match scalar_expr {
185        Expr::Alias(Alias { name, .. }) => Ok(Column::new(
186            Some::<TableReference>(subqry_alias.into()),
187            name,
188        )),
189        Expr::Column(col) => Ok(col.with_relation(subqry_alias.into())),
190        _ => {
191            let scalar_column = scalar_expr.schema_name().to_string();
192            Ok(Column::new(
193                Some::<TableReference>(subqry_alias.into()),
194                scalar_column,
195            ))
196        }
197    }
198}
199
200/// Recursively un-normalize all [`Column`] expressions in a list of expression trees
201#[inline]
202pub fn unnormalize_cols(exprs: impl IntoIterator<Item = Expr>) -> Vec<Expr> {
203    exprs.into_iter().map(unnormalize_col).collect()
204}
205
206/// Recursively remove all the ['OuterReferenceColumn'] and return the inside Column
207/// in the expression tree.
208pub fn strip_outer_reference(expr: Expr) -> Expr {
209    expr.transform(|expr| {
210        Ok({
211            if let Expr::OuterReferenceColumn(_, col) = expr {
212                Transformed::yes(Expr::Column(col))
213            } else {
214                Transformed::no(expr)
215            }
216        })
217    })
218    .data()
219    .expect("strip_outer_reference is infallible")
220}
221
222/// Returns plan with expressions coerced to types compatible with
223/// schema types
224pub fn coerce_plan_expr_for_schema(
225    plan: LogicalPlan,
226    schema: &DFSchema,
227) -> Result<LogicalPlan> {
228    match plan {
229        // special case Projection to avoid adding multiple projections
230        LogicalPlan::Projection(Projection { expr, input, .. }) => {
231            let new_exprs = coerce_exprs_for_schema(expr, input.schema(), schema)?;
232            let projection = Projection::try_new(new_exprs, input)?;
233            Ok(LogicalPlan::Projection(projection))
234        }
235        _ => {
236            let exprs: Vec<Expr> = plan.schema().iter().map(Expr::from).collect();
237            let new_exprs = coerce_exprs_for_schema(exprs, plan.schema(), schema)?;
238            let add_project = new_exprs.iter().any(|expr| expr.try_as_col().is_none());
239            if add_project {
240                let projection = Projection::try_new(new_exprs, Arc::new(plan))?;
241                Ok(LogicalPlan::Projection(projection))
242            } else {
243                Ok(plan)
244            }
245        }
246    }
247}
248
249fn coerce_exprs_for_schema(
250    exprs: Vec<Expr>,
251    src_schema: &DFSchema,
252    dst_schema: &DFSchema,
253) -> Result<Vec<Expr>> {
254    exprs
255        .into_iter()
256        .enumerate()
257        .map(|(idx, expr)| {
258            let new_type = dst_schema.field(idx).data_type();
259            if new_type != &expr.get_type(src_schema)? {
260                match expr {
261                    Expr::Alias(Alias { expr, name, .. }) => {
262                        Ok(expr.cast_to(new_type, src_schema)?.alias(name))
263                    }
264                    #[expect(deprecated)]
265                    Expr::Wildcard { .. } => Ok(expr),
266                    _ => {
267                        match expr {
268                            // maintain the original name when casting a column, to avoid the
269                            // tablename being added to it when not explicitly set by the query
270                            // (see: https://github.com/apache/datafusion/issues/18818)
271                            Expr::Column(ref column) => {
272                                let name = column.name().to_owned();
273                                Ok(expr.cast_to(new_type, src_schema)?.alias(name))
274                            }
275                            _ => Ok(expr.cast_to(new_type, src_schema)?),
276                        }
277                    }
278                }
279            } else {
280                Ok(expr)
281            }
282        })
283        .collect::<Result<_>>()
284}
285
286/// Recursively un-alias an expressions
287#[inline]
288pub fn unalias(expr: Expr) -> Expr {
289    match expr {
290        Expr::Alias(Alias { expr, .. }) => unalias(*expr),
291        _ => expr,
292    }
293}
294
295/// Handles ensuring the name of rewritten expressions is not changed.
296///
297/// This is important when optimizing plans to ensure the output
298/// schema of plan nodes don't change after optimization.
299/// For example, if an expression `1 + 2` is rewritten to `3`, the name of the
300/// expression should be preserved: `3 as "1 + 2"`
301///
302/// See <https://github.com/apache/datafusion/issues/3555> for details
303pub struct NamePreserver {
304    use_alias: bool,
305}
306
307/// If the qualified name of an expression is remembered, it will be preserved
308/// when rewriting the expression
309#[derive(Debug)]
310pub enum SavedName {
311    /// Saved qualified name to be preserved
312    Saved {
313        relation: Option<TableReference>,
314        name: String,
315    },
316    /// Name is not preserved
317    None,
318}
319
320impl NamePreserver {
321    /// Create a new NamePreserver for rewriting the `expr` that is part of the specified plan
322    pub fn new(plan: &LogicalPlan) -> Self {
323        Self {
324            // The expressions of these plans do not contribute to their output schema,
325            // so there is no need to preserve expression names to prevent a schema change.
326            use_alias: !matches!(
327                plan,
328                LogicalPlan::Filter(_)
329                    | LogicalPlan::Join(_)
330                    | LogicalPlan::TableScan(_)
331                    | LogicalPlan::Limit(_)
332                    | LogicalPlan::Statement(_)
333            ),
334        }
335    }
336
337    /// Create a new NamePreserver for rewriting the `expr`s in `Projection`
338    ///
339    /// This will use aliases
340    pub fn new_for_projection() -> Self {
341        Self { use_alias: true }
342    }
343
344    pub fn save(&self, expr: &Expr) -> SavedName {
345        if self.use_alias {
346            match expr {
347                Expr::Alias(alias) => SavedName::Saved {
348                    relation: alias.relation.clone(),
349                    name: alias.name.clone(),
350                },
351                _ => {
352                    let (relation, name) = expr.qualified_name();
353                    SavedName::Saved { relation, name }
354                }
355            }
356        } else {
357            SavedName::None
358        }
359    }
360}
361
362impl SavedName {
363    /// Ensures the qualified name of the rewritten expression is preserved
364    pub fn restore(self, expr: Expr) -> Expr {
365        match self {
366            SavedName::Saved { relation, name } => {
367                let (new_relation, new_name) = expr.qualified_name();
368                if new_relation != relation || new_name != name {
369                    expr.alias_qualified(relation, name)
370                } else {
371                    expr
372                }
373            }
374            SavedName::None => expr,
375        }
376    }
377}
378
379#[cfg(test)]
380mod test {
381    use std::ops::Add;
382
383    use super::*;
384    use crate::literal::lit_with_metadata;
385    use crate::{Cast, col, lit};
386    use arrow::datatypes::{DataType, Field, Schema};
387    use datafusion_common::ScalarValue;
388    use datafusion_common::tree_node::TreeNodeRewriter;
389
390    #[derive(Default)]
391    struct RecordingRewriter {
392        v: Vec<String>,
393    }
394
395    impl TreeNodeRewriter for RecordingRewriter {
396        type Node = Expr;
397
398        fn f_down(&mut self, expr: Expr) -> Result<Transformed<Expr>> {
399            self.v.push(format!("Previsited {expr}"));
400            Ok(Transformed::no(expr))
401        }
402
403        fn f_up(&mut self, expr: Expr) -> Result<Transformed<Expr>> {
404            self.v.push(format!("Mutated {expr}"));
405            Ok(Transformed::no(expr))
406        }
407    }
408
409    #[test]
410    fn rewriter_rewrite() {
411        // rewrites all "foo" string literals to "bar"
412        let transformer = |expr: Expr| -> Result<Transformed<Expr>> {
413            match expr {
414                Expr::Literal(ScalarValue::Utf8(Some(utf8_val)), metadata) => {
415                    let utf8_val = if utf8_val == "foo" {
416                        "bar".to_string()
417                    } else {
418                        utf8_val
419                    };
420                    Ok(Transformed::yes(lit_with_metadata(utf8_val, metadata)))
421                }
422                // otherwise, return None
423                _ => Ok(Transformed::no(expr)),
424            }
425        };
426
427        // rewrites "foo" --> "bar"
428        let rewritten = col("state")
429            .eq(lit("foo"))
430            .transform(transformer)
431            .data()
432            .unwrap();
433        assert_eq!(rewritten, col("state").eq(lit("bar")));
434
435        // doesn't rewrite
436        let rewritten = col("state")
437            .eq(lit("baz"))
438            .transform(transformer)
439            .data()
440            .unwrap();
441        assert_eq!(rewritten, col("state").eq(lit("baz")));
442    }
443
444    #[test]
445    fn normalize_cols() {
446        let expr = col("a") + col("b") + col("c");
447
448        // Schemas with some matching and some non matching cols
449        let schema_a = make_schema_with_empty_metadata(
450            vec![Some("tableA".into()), Some("tableA".into())],
451            vec!["a", "aa"],
452        );
453        let schema_c = make_schema_with_empty_metadata(
454            vec![Some("tableC".into()), Some("tableC".into())],
455            vec!["cc", "c"],
456        );
457        let schema_b =
458            make_schema_with_empty_metadata(vec![Some("tableB".into())], vec!["b"]);
459        // non matching
460        let schema_f = make_schema_with_empty_metadata(
461            vec![Some("tableC".into()), Some("tableC".into())],
462            vec!["f", "ff"],
463        );
464        let schemas = [schema_c, schema_f, schema_b, schema_a];
465        let schemas = schemas.iter().collect::<Vec<_>>();
466
467        let normalized_expr =
468            normalize_col_with_schemas_and_ambiguity_check(expr, &[&schemas], &[])
469                .unwrap();
470        assert_eq!(
471            normalized_expr,
472            col("tableA.a") + col("tableB.b") + col("tableC.c")
473        );
474    }
475
476    #[test]
477    fn normalize_cols_non_exist() {
478        // test normalizing columns when the name doesn't exist
479        let expr = col("a") + col("b");
480        let schema_a =
481            make_schema_with_empty_metadata(vec![Some("\"tableA\"".into())], vec!["a"]);
482        let schemas = [schema_a];
483        let schemas = schemas.iter().collect::<Vec<_>>();
484
485        let error =
486            normalize_col_with_schemas_and_ambiguity_check(expr, &[&schemas], &[])
487                .unwrap_err()
488                .strip_backtrace();
489        let expected = "Schema error: No field named b.\n\
490            Valid fields are \"tableA\".a.";
491        assert_eq!(error, expected);
492    }
493
494    #[test]
495    fn unnormalize_cols() {
496        let expr = col("tableA.a") + col("tableB.b");
497        let unnormalized_expr = unnormalize_col(expr);
498        assert_eq!(unnormalized_expr, col("a") + col("b"));
499    }
500
501    fn make_schema_with_empty_metadata(
502        qualifiers: Vec<Option<TableReference>>,
503        fields: Vec<&str>,
504    ) -> DFSchema {
505        let fields = fields
506            .iter()
507            .map(|f| Arc::new(Field::new((*f).to_string(), DataType::Int8, false)))
508            .collect::<Vec<_>>();
509        let schema = Arc::new(Schema::new(fields));
510        DFSchema::from_field_specific_qualified_schema(qualifiers, &schema).unwrap()
511    }
512
513    #[test]
514    fn rewriter_visit() {
515        let mut rewriter = RecordingRewriter::default();
516        col("state").eq(lit("CO")).rewrite(&mut rewriter).unwrap();
517
518        assert_eq!(
519            rewriter.v,
520            vec![
521                "Previsited state = Utf8(\"CO\")",
522                "Previsited state",
523                "Mutated state",
524                "Previsited Utf8(\"CO\")",
525                "Mutated Utf8(\"CO\")",
526                "Mutated state = Utf8(\"CO\")"
527            ]
528        )
529    }
530
531    #[test]
532    fn test_rewrite_preserving_name() {
533        test_rewrite(col("a"), col("a"));
534
535        test_rewrite(col("a"), col("b"));
536
537        // cast data types
538        test_rewrite(
539            col("a"),
540            Expr::Cast(Cast::new(Box::new(col("a")), DataType::Int32)),
541        );
542
543        // change literal type from i32 to i64
544        test_rewrite(col("a").add(lit(1i32)), col("a").add(lit(1i64)));
545
546        // test preserve qualifier
547        test_rewrite(
548            Expr::Column(Column::new(Some("test"), "a")),
549            Expr::Column(Column::new_unqualified("test.a")),
550        );
551        test_rewrite(
552            Expr::Column(Column::new_unqualified("test.a")),
553            Expr::Column(Column::new(Some("test"), "a")),
554        );
555    }
556
557    /// rewrites `expr_from` to `rewrite_to` while preserving the original qualified name
558    /// by using the `NamePreserver`
559    fn test_rewrite(expr_from: Expr, rewrite_to: Expr) {
560        struct TestRewriter {
561            rewrite_to: Expr,
562        }
563
564        impl TreeNodeRewriter for TestRewriter {
565            type Node = Expr;
566
567            fn f_up(&mut self, _: Expr) -> Result<Transformed<Expr>> {
568                Ok(Transformed::yes(self.rewrite_to.clone()))
569            }
570        }
571
572        let mut rewriter = TestRewriter {
573            rewrite_to: rewrite_to.clone(),
574        };
575        let saved_name = NamePreserver { use_alias: true }.save(&expr_from);
576        let new_expr = expr_from.clone().rewrite(&mut rewriter).unwrap().data;
577        let new_expr = saved_name.restore(new_expr);
578
579        let original_name = expr_from.qualified_name();
580        let new_name = new_expr.qualified_name();
581        assert_eq!(
582            original_name, new_name,
583            "mismatch rewriting expr_from: {expr_from} to {rewrite_to}"
584        )
585    }
586}