| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| //! See `main.rs` for how to run it. | |
| use arrow::array::{ArrayRef, Int32Array, RecordBatch, StringArray}; | |
| use arrow::datatypes::DataType; | |
| use datafusion::common::tree_node::{Transformed, TreeNode}; | |
| use datafusion::common::{Result, ScalarValue, assert_batches_eq}; | |
| use datafusion::logical_expr::{ | |
| BinaryExpr, ColumnarValue, Expr, LogicalPlan, Operator, ScalarFunctionArgs, | |
| ScalarUDF, ScalarUDFImpl, Signature, Volatility, | |
| }; | |
| use datafusion::optimizer::ApplyOrder; | |
| use datafusion::optimizer::{OptimizerConfig, OptimizerRule}; | |
| use datafusion::prelude::SessionContext; | |
| use std::sync::Arc; | |
| /// This example demonstrates how to add your own [`OptimizerRule`] | |
| /// to DataFusion. | |
| /// | |
| /// [`OptimizerRule`]s transform [`LogicalPlan`]s into an equivalent (but | |
| /// hopefully faster) form. | |
| /// | |
| /// See [analyzer_rule.rs] for an example of AnalyzerRules, which are for | |
| /// changing plan semantics. | |
| pub async fn optimizer_rule() -> Result<()> { | |
| // DataFusion includes many built in OptimizerRules for tasks such as outer | |
| // to inner join conversion and constant folding. | |
| // | |
| // Note you can change the order of optimizer rules using the lower level | |
| // `SessionState` API | |
| let ctx = SessionContext::new(); | |
| ctx.add_optimizer_rule(Arc::new(MyOptimizerRule {})); | |
| // Now, let's plan and run queries with the new rule | |
| ctx.register_batch("person", person_batch())?; | |
| let sql = "SELECT * FROM person WHERE age = 22"; | |
| let plan = ctx.sql(sql).await?.into_optimized_plan()?; | |
| // We can see the effect of our rewrite on the output plan that the filter | |
| // has been rewritten to `my_eq` | |
| assert_eq!( | |
| plan.display_indent().to_string(), | |
| "Filter: my_eq(person.age, Int32(22))\ | |
| \n TableScan: person projection=[name, age]" | |
| ); | |
| // The query below doesn't respect a filter `where age = 22` because | |
| // the plan has been rewritten using UDF which returns always true | |
| // | |
| // And the output verifies the predicates have been changed (as the my_eq | |
| // function always returns true) | |
| assert_batches_eq!( | |
| [ | |
| "+--------+-----+", | |
| "| name | age |", | |
| "+--------+-----+", | |
| "| Andy | 11 |", | |
| "| Andrew | 22 |", | |
| "| Oleks | 33 |", | |
| "+--------+-----+", | |
| ], | |
| &ctx.sql(sql).await?.collect().await? | |
| ); | |
| // however we can see the rule doesn't trigger for queries with predicates | |
| // other than `=` | |
| assert_batches_eq!( | |
| [ | |
| "+-------+-----+", | |
| "| name | age |", | |
| "+-------+-----+", | |
| "| Andy | 11 |", | |
| "| Oleks | 33 |", | |
| "+-------+-----+", | |
| ], | |
| &ctx.sql("SELECT * FROM person WHERE age <> 22") | |
| .await? | |
| .collect() | |
| .await? | |
| ); | |
| Ok(()) | |
| } | |
| /// An example OptimizerRule that replaces all `col = <const>` predicates with a | |
| /// user defined function | |
| #[derive(Default, Debug)] | |
| struct MyOptimizerRule {} | |
| impl OptimizerRule for MyOptimizerRule { | |
| fn name(&self) -> &str { | |
| "my_optimizer_rule" | |
| } | |
| /// Ask the optimizer to handle the plan recursion. `rewrite` will be called | |
| /// on each plan node. | |
| fn apply_order(&self) -> Option<ApplyOrder> { | |
| Some(ApplyOrder::BottomUp) | |
| } | |
| fn rewrite( | |
| &self, | |
| plan: LogicalPlan, | |
| _config: &dyn OptimizerConfig, | |
| ) -> Result<Transformed<LogicalPlan>> { | |
| plan.map_expressions(|expr| { | |
| // This closure is called for all expressions in the current plan | |
| // | |
| // For example, given a plan like `SELECT a + b, 5 + 10` | |
| // | |
| // The closure would be called twice: | |
| // 1. once for `a + b` | |
| // 2. once for `5 + 10` | |
| self.rewrite_expr(expr) | |
| }) | |
| } | |
| } | |
| impl MyOptimizerRule { | |
| /// Rewrites an Expr replacing all `<col> = <const>` expressions with | |
| /// a call to my_eq udf | |
| fn rewrite_expr(&self, expr: Expr) -> Result<Transformed<Expr>> { | |
| // do a bottom up rewrite of the expression tree | |
| expr.transform_up(|expr| { | |
| // Closure called for each sub tree | |
| match expr { | |
| Expr::BinaryExpr(binary_expr) if is_binary_eq(&binary_expr) => { | |
| // destructure the expression | |
| let BinaryExpr { left, op: _, right } = binary_expr; | |
| // rewrite to `my_eq(left, right)` | |
| let udf = ScalarUDF::new_from_impl(MyEq::new()); | |
| let call = udf.call(vec![*left, *right]); | |
| Ok(Transformed::yes(call)) | |
| } | |
| _ => Ok(Transformed::no(expr)), | |
| } | |
| }) | |
| // Note that the TreeNode API handles propagating the transformed flag | |
| // and errors up the call chain | |
| } | |
| } | |
| /// return true of the expression is an equality expression for a literal or | |
| /// column reference | |
| fn is_binary_eq(binary_expr: &BinaryExpr) -> bool { | |
| binary_expr.op == Operator::Eq | |
| && is_lit_or_col(binary_expr.left.as_ref()) | |
| && is_lit_or_col(binary_expr.right.as_ref()) | |
| } | |
| /// Return true if the expression is a literal or column reference | |
| fn is_lit_or_col(expr: &Expr) -> bool { | |
| matches!(expr, Expr::Column(_) | Expr::Literal(_, _)) | |
| } | |
| /// A simple user defined filter function | |
| #[derive(Debug, PartialEq, Eq, Hash, Clone)] | |
| struct MyEq { | |
| signature: Signature, | |
| } | |
| impl MyEq { | |
| fn new() -> Self { | |
| Self { | |
| signature: Signature::any(2, Volatility::Stable), | |
| } | |
| } | |
| } | |
| impl ScalarUDFImpl for MyEq { | |
| fn name(&self) -> &str { | |
| "my_eq" | |
| } | |
| fn signature(&self) -> &Signature { | |
| &self.signature | |
| } | |
| fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> { | |
| Ok(DataType::Boolean) | |
| } | |
| fn invoke_with_args(&self, _args: ScalarFunctionArgs) -> Result<ColumnarValue> { | |
| // this example simply returns "true" which is not what a real | |
| // implementation would do. | |
| Ok(ColumnarValue::Scalar(ScalarValue::from(true))) | |
| } | |
| } | |
| /// Return a RecordBatch with made up data | |
| fn person_batch() -> RecordBatch { | |
| let name: ArrayRef = | |
| Arc::new(StringArray::from_iter_values(["Andy", "Andrew", "Oleks"])); | |
| let age: ArrayRef = Arc::new(Int32Array::from(vec![11, 22, 33])); | |
| RecordBatch::try_from_iter(vec![("name", name), ("age", age)]).unwrap() | |
| } |