This is an automated email from the ASF dual-hosted git repository.
agrove pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-datafusion.git
The following commit(s) were added to refs/heads/master by this push:
new 10e64dc01 Refactor: move `simplify_expression.rs` and
`expr_simplifier.rs` to a new mod `simplify_expressions` (#3951)
10e64dc01 is described below
commit 10e64dc013ba210ab1f6c2a3c02c66aef4a0e802
Author: Remzi Yang <[email protected]>
AuthorDate: Wed Oct 26 08:53:56 2022 +0800
Refactor: move `simplify_expression.rs` and `expr_simplifier.rs` to a new
mod `simplify_expressions` (#3951)
* create mod for expr_simpl
Signed-off-by: remzi <[email protected]>
* move utility to utils
Signed-off-by: remzi <[email protected]>
* create context mod
Signed-off-by: remzi <[email protected]>
* add docs for context
Signed-off-by: remzi <[email protected]>
* move constEvalutor to expr_simplififer
Signed-off-by: remzi <[email protected]>
* move simplifier to expr_simplifier
Signed-off-by: remzi <[email protected]>
* fix lints
Signed-off-by: remzi <[email protected]>
* fix merge conflict
Signed-off-by: remzi <[email protected]>
Signed-off-by: remzi <[email protected]>
---
datafusion-examples/examples/expr_api.rs | 2 +-
datafusion/core/tests/simplification.rs | 2 +-
datafusion/expr/src/lib.rs | 4 +-
datafusion/optimizer/src/expr_simplifier.rs | 315 ---
datafusion/optimizer/src/lib.rs | 1 -
.../optimizer/src/simplify_expressions/context.rs | 130 ++
.../expr_simplifier.rs} | 2062 +++++---------------
.../src/{lib.rs => simplify_expressions/mod.rs} | 32 +-
.../src/simplify_expressions/simplify_exprs.rs | 846 ++++++++
.../optimizer/src/simplify_expressions/utils.rs | 369 ++++
10 files changed, 1897 insertions(+), 1866 deletions(-)
diff --git a/datafusion-examples/examples/expr_api.rs
b/datafusion-examples/examples/expr_api.rs
index 61228946f..97abf4d55 100644
--- a/datafusion-examples/examples/expr_api.rs
+++ b/datafusion-examples/examples/expr_api.rs
@@ -17,7 +17,7 @@
use datafusion::arrow::datatypes::{DataType, Field, Schema, TimeUnit};
use datafusion::error::Result;
-use datafusion::optimizer::expr_simplifier::{ExprSimplifier, SimplifyContext};
+use datafusion::optimizer::simplify_expressions::{ExprSimplifier,
SimplifyContext};
use datafusion::physical_expr::execution_props::ExecutionProps;
use datafusion::prelude::*;
use datafusion_common::{ScalarValue, ToDFSchema};
diff --git a/datafusion/core/tests/simplification.rs
b/datafusion/core/tests/simplification.rs
index 5a4a2e92f..6e74fc0d9 100644
--- a/datafusion/core/tests/simplification.rs
+++ b/datafusion/core/tests/simplification.rs
@@ -21,7 +21,7 @@ use arrow::datatypes::{DataType, Field, Schema};
use datafusion::common::DFSchema;
use datafusion::{error::Result, execution::context::ExecutionProps,
prelude::*};
use datafusion_expr::{Expr, ExprSchemable};
-use datafusion_optimizer::expr_simplifier::{ExprSimplifier, SimplifyInfo};
+use datafusion_optimizer::simplify_expressions::{ExprSimplifier, SimplifyInfo};
/// In order to simplify expressions, DataFusion must have information
/// about the expressions.
diff --git a/datafusion/expr/src/lib.rs b/datafusion/expr/src/lib.rs
index 0fcdf5546..874a80713 100644
--- a/datafusion/expr/src/lib.rs
+++ b/datafusion/expr/src/lib.rs
@@ -56,7 +56,9 @@ pub use accumulator::{Accumulator, AggregateState};
pub use aggregate_function::AggregateFunction;
pub use built_in_function::BuiltinScalarFunction;
pub use columnar_value::{ColumnarValue, NullColumnarValue};
-pub use expr::{Between, BinaryExpr, Case, Expr, GetIndexedField, GroupingSet,
Like};
+pub use expr::{
+ Between, BinaryExpr, Case, Cast, Expr, GetIndexedField, GroupingSet, Like,
+};
pub use expr_fn::*;
pub use expr_schema::ExprSchemable;
pub use function::{
diff --git a/datafusion/optimizer/src/expr_simplifier.rs
b/datafusion/optimizer/src/expr_simplifier.rs
deleted file mode 100644
index a30fa9c3b..000000000
--- a/datafusion/optimizer/src/expr_simplifier.rs
+++ /dev/null
@@ -1,315 +0,0 @@
-// 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.
-
-//! Expression simplification API
-
-use crate::{
- simplify_expressions::{ConstEvaluator, Simplifier},
- type_coercion::TypeCoercionRewriter,
-};
-use arrow::datatypes::DataType;
-use datafusion_common::{DFSchemaRef, DataFusionError, Result};
-use datafusion_expr::{expr_rewriter::ExprRewritable, Expr, ExprSchemable};
-use datafusion_physical_expr::execution_props::ExecutionProps;
-
-#[allow(rustdoc::private_intra_doc_links)]
-/// The information necessary to apply algebraic simplification to an
-/// [Expr]. See [SimplifyContext] for one concrete implementation.
-///
-/// This trait exists so that other systems can plug schema
-/// information in without having to create `DFSchema` objects. If you
-/// have a [`DFSchemaRef`] you can use [`SimplifyContext`]
-pub trait SimplifyInfo {
- /// returns true if this Expr has boolean type
- fn is_boolean_type(&self, expr: &Expr) -> Result<bool>;
-
- /// returns true of this expr is nullable (could possibly be NULL)
- fn nullable(&self, expr: &Expr) -> Result<bool>;
-
- /// Returns details needed for partial expression evaluation
- fn execution_props(&self) -> &ExecutionProps;
-}
-
-/// This structure handles API for expression simplification
-pub struct ExprSimplifier<S> {
- info: S,
-}
-
-impl<S: SimplifyInfo> ExprSimplifier<S> {
- /// Create a new `ExprSimplifier` with the given `info` such as an
- /// instance of [`SimplifyContext`]. See
- /// [`simplify`](Self::simplify) for an example.
- pub fn new(info: S) -> Self {
- Self { info }
- }
-
- /// Simplifies this [`Expr`]`s as much as possible, evaluating
- /// constants and applying algebraic simplifications.
- ///
- /// The types of the expression must match what operators expect,
- /// or else an error may occur trying to evaluate. See
- /// [`coerce`](Self::coerce) for a function to help.
- ///
- /// # Example:
- ///
- /// `b > 2 AND b > 2`
- ///
- /// can be written to
- ///
- /// `b > 2`
- ///
- /// ```
- /// use datafusion_expr::{col, lit, Expr};
- /// use datafusion_common::Result;
- /// use datafusion_physical_expr::execution_props::ExecutionProps;
- /// use datafusion_optimizer::expr_simplifier::{ExprSimplifier,
SimplifyInfo};
- ///
- /// /// Simple implementation that provides `Simplifier` the information
it needs
- /// /// See SimplifyContext for a structure that does this.
- /// #[derive(Default)]
- /// struct Info {
- /// execution_props: ExecutionProps,
- /// };
- ///
- /// impl SimplifyInfo for Info {
- /// fn is_boolean_type(&self, expr: &Expr) -> Result<bool> {
- /// Ok(false)
- /// }
- /// fn nullable(&self, expr: &Expr) -> Result<bool> {
- /// Ok(true)
- /// }
- /// fn execution_props(&self) -> &ExecutionProps {
- /// &self.execution_props
- /// }
- /// }
- ///
- /// // Create the simplifier
- /// let simplifier = ExprSimplifier::new(Info::default());
- ///
- /// // b < 2
- /// let b_lt_2 = col("b").gt(lit(2));
- ///
- /// // (b < 2) OR (b < 2)
- /// let expr = b_lt_2.clone().or(b_lt_2.clone());
- ///
- /// // (b < 2) OR (b < 2) --> (b < 2)
- /// let expr = simplifier.simplify(expr).unwrap();
- /// assert_eq!(expr, b_lt_2);
- /// ```
- pub fn simplify(&self, expr: Expr) -> Result<Expr> {
- let mut simplifier = Simplifier::new(&self.info);
- let mut const_evaluator =
ConstEvaluator::try_new(self.info.execution_props())?;
-
- // TODO iterate until no changes are made during rewrite
- // (evaluating constants can enable new simplifications and
- // simplifications can enable new constant evaluation)
- // https://github.com/apache/arrow-datafusion/issues/1160
- expr.rewrite(&mut const_evaluator)?
- .rewrite(&mut simplifier)?
- // run both passes twice to try an minimize simplifications that
we missed
- .rewrite(&mut const_evaluator)?
- .rewrite(&mut simplifier)
- }
-
- /// Apply type coercion to an [`Expr`] so that it can be
- /// evaluated as a
[`PhysicalExpr`](datafusion_physical_expr::PhysicalExpr).
- ///
- /// See the [type coercion module](datafusion_expr::type_coercion)
- /// documentation for more details on type coercion
- ///
- // Would be nice if this API could use the SimplifyInfo
- // rather than creating an DFSchemaRef coerces rather than doing
- // it manually.
- // https://github.com/apache/arrow-datafusion/issues/3793
- pub fn coerce(&self, expr: Expr, schema: DFSchemaRef) -> Result<Expr> {
- let mut expr_rewrite = TypeCoercionRewriter { schema };
-
- expr.rewrite(&mut expr_rewrite)
- }
-}
-
-/// Provides simplification information based on DFSchema and
-/// [`ExecutionProps`]. This is the default implementation used by DataFusion
-///
-/// For example:
-/// ```
-/// use arrow::datatypes::{Schema, Field, DataType};
-/// use datafusion_expr::{col, lit};
-/// use datafusion_common::{DataFusionError, ToDFSchema};
-/// use datafusion_physical_expr::execution_props::ExecutionProps;
-/// use datafusion_optimizer::expr_simplifier::{SimplifyContext,
ExprSimplifier};
-///
-/// // Create the schema
-/// let schema = Schema::new(vec![
-/// Field::new("i", DataType::Int64, false),
-/// ])
-/// .to_dfschema_ref().unwrap();
-///
-/// // Create the simplifier
-/// let props = ExecutionProps::new();
-/// let context = SimplifyContext::new(&props)
-/// .with_schema(schema);
-/// let simplifier = ExprSimplifier::new(context);
-///
-/// // Use the simplifier
-///
-/// // b < 2 or (1 > 3)
-/// let expr = col("b").lt(lit(2)).or(lit(1).gt(lit(3)));
-///
-/// // b < 2
-/// let simplified = simplifier.simplify(expr).unwrap();
-/// assert_eq!(simplified, col("b").lt(lit(2)));
-/// ```
-pub struct SimplifyContext<'a> {
- schemas: Vec<DFSchemaRef>,
- props: &'a ExecutionProps,
-}
-
-impl<'a> SimplifyContext<'a> {
- /// Create a new SimplifyContext
- pub fn new(props: &'a ExecutionProps) -> Self {
- Self {
- schemas: vec![],
- props,
- }
- }
-
- /// Register a [`DFSchemaRef`] with this context
- pub fn with_schema(mut self, schema: DFSchemaRef) -> Self {
- self.schemas.push(schema);
- self
- }
-}
-
-impl<'a> SimplifyInfo for SimplifyContext<'a> {
- /// returns true if this Expr has boolean type
- fn is_boolean_type(&self, expr: &Expr) -> Result<bool> {
- for schema in &self.schemas {
- if let Ok(DataType::Boolean) = expr.get_type(schema) {
- return Ok(true);
- }
- }
-
- Ok(false)
- }
- /// Returns true if expr is nullable
- fn nullable(&self, expr: &Expr) -> Result<bool> {
- self.schemas
- .iter()
- .find_map(|schema| {
- // expr may be from another input, so ignore errors
- // by converting to None to keep trying
- expr.nullable(schema.as_ref()).ok()
- })
- .ok_or_else(|| {
- // This means we weren't able to compute `Expr::nullable` with
- // *any* input schemas, signalling a problem
- DataFusionError::Internal(format!(
- "Could not find columns in '{}' during simplify",
- expr
- ))
- })
- }
-
- fn execution_props(&self) -> &ExecutionProps {
- self.props
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use arrow::datatypes::{Field, Schema};
- use datafusion_common::ToDFSchema;
- use datafusion_expr::{col, lit, when};
-
- #[test]
- fn api_basic() {
- let props = ExecutionProps::new();
- let simplifier =
-
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(test_schema()));
-
- let expr = lit(1) + lit(2);
- let expected = lit(3);
- assert_eq!(expected, simplifier.simplify(expr).unwrap());
- }
-
- #[test]
- fn basic_coercion() {
- let schema = test_schema();
- let props = ExecutionProps::new();
- let simplifier =
-
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(schema.clone()));
-
- // Note expr type is int32 (not int64)
- // (1i64 + 2i32) < i
- let expr = (lit(1i64) + lit(2i32)).lt(col("i"));
- // should fully simplify to 3 < i (though i has been coerced to i64)
- let expected = lit(3i64).lt(col("i"));
-
- // Would be nice if this API could use the SimplifyInfo
- // rather than creating an DFSchemaRef coerces rather than doing
- // it manually.
- // https://github.com/apache/arrow-datafusion/issues/3793
- let expr = simplifier.coerce(expr, schema).unwrap();
-
- assert_eq!(expected, simplifier.simplify(expr).unwrap());
- }
-
- fn test_schema() -> DFSchemaRef {
- Schema::new(vec![
- Field::new("i", DataType::Int64, false),
- Field::new("b", DataType::Boolean, true),
- ])
- .to_dfschema_ref()
- .unwrap()
- }
-
- #[test]
- fn simplify_and_constant_prop() {
- let props = ExecutionProps::new();
- let simplifier =
-
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(test_schema()));
-
- // should be able to simplify to false
- // (i * (1 - 2)) > 0
- let expr = (col("i") * (lit(1) - lit(1))).gt(lit(0));
- let expected = lit(false);
- assert_eq!(expected, simplifier.simplify(expr).unwrap());
- }
-
- #[test]
- fn simplify_and_constant_prop_with_case() {
- let props = ExecutionProps::new();
- let simplifier =
-
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(test_schema()));
-
- // CASE
- // WHEN i>5 AND false THEN i > 5
- // WHEN i<5 AND true THEN i < 5
- // ELSE false
- // END
- //
- // Can be simplified to `i < 5`
- let expr = when(col("i").gt(lit(5)).and(lit(false)),
col("i").gt(lit(5)))
- .when(col("i").lt(lit(5)).and(lit(true)), col("i").lt(lit(5)))
- .otherwise(lit(false))
- .unwrap();
- let expected = col("i").lt(lit(5));
- assert_eq!(expected, simplifier.simplify(expr).unwrap());
- }
-}
diff --git a/datafusion/optimizer/src/lib.rs b/datafusion/optimizer/src/lib.rs
index 5e8108d67..13d4cf4a3 100644
--- a/datafusion/optimizer/src/lib.rs
+++ b/datafusion/optimizer/src/lib.rs
@@ -20,7 +20,6 @@ pub mod decorrelate_where_exists;
pub mod decorrelate_where_in;
pub mod eliminate_filter;
pub mod eliminate_limit;
-pub mod expr_simplifier;
pub mod filter_null_join_keys;
pub mod filter_push_down;
pub mod inline_table_scan;
diff --git a/datafusion/optimizer/src/simplify_expressions/context.rs
b/datafusion/optimizer/src/simplify_expressions/context.rs
new file mode 100644
index 000000000..da44a7e8f
--- /dev/null
+++ b/datafusion/optimizer/src/simplify_expressions/context.rs
@@ -0,0 +1,130 @@
+// 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.
+
+//! Structs and traits to provide the information needed for expression
simplification.
+
+use arrow::datatypes::DataType;
+use datafusion_common::{DFSchemaRef, DataFusionError, Result};
+use datafusion_expr::{Expr, ExprSchemable};
+use datafusion_physical_expr::execution_props::ExecutionProps;
+
+#[allow(rustdoc::private_intra_doc_links)]
+/// The information necessary to apply algebraic simplification to an
+/// [Expr]. See [SimplifyContext] for one concrete implementation.
+///
+/// This trait exists so that other systems can plug schema
+/// information in without having to create `DFSchema` objects. If you
+/// have a [`DFSchemaRef`] you can use [`SimplifyContext`]
+pub trait SimplifyInfo {
+ /// returns true if this Expr has boolean type
+ fn is_boolean_type(&self, expr: &Expr) -> Result<bool>;
+
+ /// returns true of this expr is nullable (could possibly be NULL)
+ fn nullable(&self, expr: &Expr) -> Result<bool>;
+
+ /// Returns details needed for partial expression evaluation
+ fn execution_props(&self) -> &ExecutionProps;
+}
+
+/// Provides simplification information based on DFSchema and
+/// [`ExecutionProps`]. This is the default implementation used by DataFusion
+///
+/// For example:
+/// ```
+/// use arrow::datatypes::{Schema, Field, DataType};
+/// use datafusion_expr::{col, lit};
+/// use datafusion_common::{DataFusionError, ToDFSchema};
+/// use datafusion_physical_expr::execution_props::ExecutionProps;
+/// use datafusion_optimizer::simplify_expressions::{SimplifyContext,
ExprSimplifier};
+///
+/// // Create the schema
+/// let schema = Schema::new(vec![
+/// Field::new("i", DataType::Int64, false),
+/// ])
+/// .to_dfschema_ref().unwrap();
+///
+/// // Create the simplifier
+/// let props = ExecutionProps::new();
+/// let context = SimplifyContext::new(&props)
+/// .with_schema(schema);
+/// let simplifier = ExprSimplifier::new(context);
+///
+/// // Use the simplifier
+///
+/// // b < 2 or (1 > 3)
+/// let expr = col("b").lt(lit(2)).or(lit(1).gt(lit(3)));
+///
+/// // b < 2
+/// let simplified = simplifier.simplify(expr).unwrap();
+/// assert_eq!(simplified, col("b").lt(lit(2)));
+/// ```
+pub struct SimplifyContext<'a> {
+ schemas: Vec<DFSchemaRef>,
+ props: &'a ExecutionProps,
+}
+
+impl<'a> SimplifyContext<'a> {
+ /// Create a new SimplifyContext
+ pub fn new(props: &'a ExecutionProps) -> Self {
+ Self {
+ schemas: vec![],
+ props,
+ }
+ }
+
+ /// Register a [`DFSchemaRef`] with this context
+ pub fn with_schema(mut self, schema: DFSchemaRef) -> Self {
+ self.schemas.push(schema);
+ self
+ }
+}
+
+impl<'a> SimplifyInfo for SimplifyContext<'a> {
+ /// returns true if this Expr has boolean type
+ fn is_boolean_type(&self, expr: &Expr) -> Result<bool> {
+ for schema in &self.schemas {
+ if let Ok(DataType::Boolean) = expr.get_type(schema) {
+ return Ok(true);
+ }
+ }
+
+ Ok(false)
+ }
+
+ /// Returns true if expr is nullable
+ fn nullable(&self, expr: &Expr) -> Result<bool> {
+ self.schemas
+ .iter()
+ .find_map(|schema| {
+ // expr may be from another input, so ignore errors
+ // by converting to None to keep trying
+ expr.nullable(schema.as_ref()).ok()
+ })
+ .ok_or_else(|| {
+ // This means we weren't able to compute `Expr::nullable` with
+ // *any* input schemas, signalling a problem
+ DataFusionError::Internal(format!(
+ "Could not find columns in '{}' during simplify",
+ expr
+ ))
+ })
+ }
+
+ fn execution_props(&self) -> &ExecutionProps {
+ self.props
+ }
+}
diff --git a/datafusion/optimizer/src/simplify_expressions.rs
b/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs
similarity index 57%
rename from datafusion/optimizer/src/simplify_expressions.rs
rename to datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs
index 32c8c9bce..6a3723644 100644
--- a/datafusion/optimizer/src/simplify_expressions.rs
+++ b/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs
@@ -15,442 +15,121 @@
// specific language governing permissions and limitations
// under the License.
-//! Simplify expressions optimizer rule and implementation
-
-use crate::expr_simplifier::{ExprSimplifier, SimplifyContext};
-use crate::{expr_simplifier::SimplifyInfo, OptimizerConfig, OptimizerRule};
-use arrow::array::new_null_array;
-use arrow::datatypes::{DataType, Field, Schema, DECIMAL128_MAX_PRECISION};
-use arrow::error::ArrowError;
-use arrow::record_batch::RecordBatch;
-use datafusion_common::{DFSchema, DataFusionError, Result, ScalarValue};
-use datafusion_expr::expr::BinaryExpr;
+//! Expression simplification API
+
+use super::utils::*;
+use crate::type_coercion::TypeCoercionRewriter;
+use arrow::{
+ array::new_null_array,
+ datatypes::{DataType, Field, Schema},
+ error::ArrowError,
+ record_batch::RecordBatch,
+};
+use datafusion_common::{DFSchema, DFSchemaRef, DataFusionError, Result,
ScalarValue};
use datafusion_expr::{
- expr::Between,
- expr_fn::{and, concat_ws, or},
+ and,
expr_rewriter::{ExprRewritable, ExprRewriter, RewriteRecursion},
- lit,
- logical_plan::LogicalPlan,
- utils::from_plan,
- BuiltinScalarFunction, ColumnarValue, Expr, Operator, Volatility,
+ lit, or, BinaryExpr, BuiltinScalarFunction, ColumnarValue, Expr,
Volatility,
};
use datafusion_physical_expr::{create_physical_expr,
execution_props::ExecutionProps};
-static POWS_OF_TEN: [i128; 38] = [
- 1,
- 10,
- 100,
- 1000,
- 10000,
- 100000,
- 1000000,
- 10000000,
- 100000000,
- 1000000000,
- 10000000000,
- 100000000000,
- 1000000000000,
- 10000000000000,
- 100000000000000,
- 1000000000000000,
- 10000000000000000,
- 100000000000000000,
- 1000000000000000000,
- 10000000000000000000,
- 100000000000000000000,
- 1000000000000000000000,
- 10000000000000000000000,
- 100000000000000000000000,
- 1000000000000000000000000,
- 10000000000000000000000000,
- 100000000000000000000000000,
- 1000000000000000000000000000,
- 10000000000000000000000000000,
- 100000000000000000000000000000,
- 1000000000000000000000000000000,
- 10000000000000000000000000000000,
- 100000000000000000000000000000000,
- 1000000000000000000000000000000000,
- 10000000000000000000000000000000000,
- 100000000000000000000000000000000000,
- 1000000000000000000000000000000000000,
- 10000000000000000000000000000000000000,
-];
-
-/// Optimizer Pass that simplifies [`LogicalPlan`]s by rewriting
-/// [`Expr`]`s evaluating constants and applying algebraic
-/// simplifications
-///
-/// # Introduction
-/// It uses boolean algebra laws to simplify or reduce the number of terms in
expressions.
-///
-/// # Example:
-/// `Filter: b > 2 AND b > 2`
-/// is optimized to
-/// `Filter: b > 2`
-///
-#[derive(Default)]
-pub struct SimplifyExpressions {}
-
-/// returns true if `needle` is found in a chain of search_op
-/// expressions. Such as: (A AND B) AND C
-fn expr_contains(expr: &Expr, needle: &Expr, search_op: Operator) -> bool {
- match expr {
- Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == search_op
=> {
- expr_contains(left, needle, search_op)
- || expr_contains(right, needle, search_op)
- }
- _ => expr == needle,
- }
-}
-
-fn is_zero(s: &Expr) -> bool {
- match s {
- Expr::Literal(ScalarValue::Int8(Some(0)))
- | Expr::Literal(ScalarValue::Int16(Some(0)))
- | Expr::Literal(ScalarValue::Int32(Some(0)))
- | Expr::Literal(ScalarValue::Int64(Some(0)))
- | Expr::Literal(ScalarValue::UInt8(Some(0)))
- | Expr::Literal(ScalarValue::UInt16(Some(0)))
- | Expr::Literal(ScalarValue::UInt32(Some(0)))
- | Expr::Literal(ScalarValue::UInt64(Some(0))) => true,
- Expr::Literal(ScalarValue::Float32(Some(v))) if *v == 0. => true,
- Expr::Literal(ScalarValue::Float64(Some(v))) if *v == 0. => true,
- Expr::Literal(ScalarValue::Decimal128(Some(v), _p, _s)) if *v == 0 =>
true,
- _ => false,
- }
-}
-
-fn is_one(s: &Expr) -> bool {
- match s {
- Expr::Literal(ScalarValue::Int8(Some(1)))
- | Expr::Literal(ScalarValue::Int16(Some(1)))
- | Expr::Literal(ScalarValue::Int32(Some(1)))
- | Expr::Literal(ScalarValue::Int64(Some(1)))
- | Expr::Literal(ScalarValue::UInt8(Some(1)))
- | Expr::Literal(ScalarValue::UInt16(Some(1)))
- | Expr::Literal(ScalarValue::UInt32(Some(1)))
- | Expr::Literal(ScalarValue::UInt64(Some(1))) => true,
- Expr::Literal(ScalarValue::Float32(Some(v))) if *v == 1. => true,
- Expr::Literal(ScalarValue::Float64(Some(v))) if *v == 1. => true,
- Expr::Literal(ScalarValue::Decimal128(Some(v), _p, _s)) => {
- *_s < DECIMAL128_MAX_PRECISION && POWS_OF_TEN[*_s as usize] == *v
- }
- _ => false,
- }
-}
-
-fn is_true(expr: &Expr) -> bool {
- match expr {
- Expr::Literal(ScalarValue::Boolean(Some(v))) => *v,
- _ => false,
- }
-}
-
-/// returns true if expr is a
-/// `Expr::Literal(ScalarValue::Boolean(v))` , false otherwise
-fn is_bool_lit(expr: &Expr) -> bool {
- matches!(expr, Expr::Literal(ScalarValue::Boolean(_)))
-}
-
-/// Return a literal NULL value of Boolean data type
-fn lit_bool_null() -> Expr {
- Expr::Literal(ScalarValue::Boolean(None))
-}
-
-fn is_null(expr: &Expr) -> bool {
- match expr {
- Expr::Literal(v) => v.is_null(),
- _ => false,
- }
-}
-
-fn is_false(expr: &Expr) -> bool {
- match expr {
- Expr::Literal(ScalarValue::Boolean(Some(v))) => !(*v),
- _ => false,
- }
-}
-
-/// returns true if `haystack` looks like (needle OP X) or (X OP needle)
-fn is_op_with(target_op: Operator, haystack: &Expr, needle: &Expr) -> bool {
- matches!(haystack, Expr::BinaryExpr(BinaryExpr { left, op, right }) if op
== &target_op && (needle == left.as_ref() || needle == right.as_ref()))
-}
-
-/// returns the contained boolean value in `expr` as
-/// `Expr::Literal(ScalarValue::Boolean(v))`.
-fn as_bool_lit(expr: Expr) -> Result<Option<bool>> {
- match expr {
- Expr::Literal(ScalarValue::Boolean(v)) => Ok(v),
- _ => Err(DataFusionError::Internal(format!(
- "Expected boolean literal, got {:?}",
- expr
- ))),
- }
-}
-
-/// negate a Not clause
-/// input is the clause to be negated.(args of Not clause)
-/// For BinaryExpr, use the negator of op instead.
-/// not ( A > B) ===> (A <= B)
-/// For BoolExpr, not (A and B) ===> (not A) or (not B)
-/// not (A or B) ===> (not A) and (not B)
-/// not (not A) ===> A
-/// For NullExpr, not (A is not null) ===> A is null
-/// not (A is null) ===> A is not null
-/// For InList, not (A not in (..)) ===> A in (..)
-/// not (A in (..)) ===> A not in (..)
-/// For Between, not (A between B and C) ===> (A not between B and C)
-/// not (A not between B and C) ===> (A between B and C)
-/// For others, use Not clause
-fn negate_clause(expr: Expr) -> Expr {
- match expr {
- Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
- if let Some(negated_op) = op.negate() {
- return Expr::BinaryExpr(BinaryExpr::new(left, negated_op,
right));
- }
- match op {
- // not (A and B) ===> (not A) or (not B)
- Operator::And => {
- let left = negate_clause(*left);
- let right = negate_clause(*right);
-
- or(left, right)
- }
- // not (A or B) ===> (not A) and (not B)
- Operator::Or => {
- let left = negate_clause(*left);
- let right = negate_clause(*right);
-
- and(left, right)
- }
- // use not clause
- _ => Expr::Not(Box::new(Expr::BinaryExpr(BinaryExpr::new(
- left, op, right,
- )))),
- }
- }
- // not (not A) ===> A
- Expr::Not(expr) => *expr,
- // not (A is not null) ===> A is null
- Expr::IsNotNull(expr) => expr.is_null(),
- // not (A is null) ===> A is not null
- Expr::IsNull(expr) => expr.is_not_null(),
- // not (A not in (..)) ===> A in (..)
- // not (A in (..)) ===> A not in (..)
- Expr::InList {
- expr,
- list,
- negated,
- } => expr.in_list(list, !negated),
- // not (A between B and C) ===> (A not between B and C)
- // not (A not between B and C) ===> (A between B and C)
- Expr::Between(between) => Expr::Between(Between::new(
- between.expr,
- !between.negated,
- between.low,
- between.high,
- )),
- // use not clause
- _ => Expr::Not(Box::new(expr)),
- }
-}
-
-/// Simplify the `concat` function by
-/// 1. filtering out all `null` literals
-/// 2. concatenating contiguous literal arguments
-///
-/// For example:
-/// `concat(col(a), 'hello ', 'world', col(b), null)`
-/// will be optimized to
-/// `concat(col(a), 'hello world', col(b))`
-fn simpl_concat(args: Vec<Expr>) -> Result<Expr> {
- let mut new_args = Vec::with_capacity(args.len());
- let mut contiguous_scalar = "".to_string();
- for arg in args {
- match arg {
- // filter out `null` args
- Expr::Literal(ScalarValue::Utf8(None) |
ScalarValue::LargeUtf8(None)) => {}
- // All literals have been converted to Utf8 or LargeUtf8 in
type_coercion.
- // Concatenate it with the `contiguous_scalar`.
- Expr::Literal(
- ScalarValue::Utf8(Some(v)) | ScalarValue::LargeUtf8(Some(v)),
- ) => contiguous_scalar += &v,
- Expr::Literal(x) => {
- return Err(DataFusionError::Internal(format!(
- "The scalar {} should be casted to string type during the
type coercion.",
- x
- )));
- }
- // If the arg is not a literal, we should first push the current
`contiguous_scalar`
- // to the `new_args` (if it is not empty) and reset it to empty
string.
- // Then pushing this arg to the `new_args`.
- arg => {
- if !contiguous_scalar.is_empty() {
- new_args.push(lit(contiguous_scalar));
- contiguous_scalar = "".to_string();
- }
- new_args.push(arg);
- }
- }
- }
- if !contiguous_scalar.is_empty() {
- new_args.push(lit(contiguous_scalar));
- }
-
- Ok(Expr::ScalarFunction {
- fun: BuiltinScalarFunction::Concat,
- args: new_args,
- })
-}
+use super::SimplifyInfo;
-/// Simply the `concat_ws` function by
-/// 1. folding to `null` if the delimiter is null
-/// 2. filtering out `null` arguments
-/// 3. using `concat` to replace `concat_ws` if the delimiter is an empty
string
-/// 4. concatenating contiguous literals if the delimiter is a literal.
-fn simpl_concat_ws(delimiter: &Expr, args: &[Expr]) -> Result<Expr> {
- match delimiter {
- Expr::Literal(
- ScalarValue::Utf8(delimiter) | ScalarValue::LargeUtf8(delimiter),
- ) => {
- match delimiter {
- // when the delimiter is an empty string,
- // we can use `concat` to replace `concat_ws`
- Some(delimiter) if delimiter.is_empty() =>
simpl_concat(args.to_vec()),
- Some(delimiter) => {
- let mut new_args = Vec::with_capacity(args.len());
- new_args.push(lit(delimiter));
- let mut contiguous_scalar = None;
- for arg in args {
- match arg {
- // filter out null args
- Expr::Literal(ScalarValue::Utf8(None) |
ScalarValue::LargeUtf8(None)) => {}
- Expr::Literal(ScalarValue::Utf8(Some(v)) |
ScalarValue::LargeUtf8(Some(v))) => {
- match contiguous_scalar {
- None => contiguous_scalar =
Some(v.to_string()),
- Some(mut pre) => {
- pre += delimiter;
- pre += v;
- contiguous_scalar = Some(pre)
- }
- }
- }
- Expr::Literal(s) => return
Err(DataFusionError::Internal(format!("The scalar {} should be casted to string
type during the type coercion.", s))),
- // If the arg is not a literal, we should first
push the current `contiguous_scalar`
- // to the `new_args` and reset it to None.
- // Then pushing this arg to the `new_args`.
- arg => {
- if let Some(val) = contiguous_scalar {
- new_args.push(lit(val));
- }
- new_args.push(arg.clone());
- contiguous_scalar = None;
- }
- }
- }
- if let Some(val) = contiguous_scalar {
- new_args.push(lit(val));
- }
- Ok(Expr::ScalarFunction {
- fun: BuiltinScalarFunction::ConcatWithSeparator,
- args: new_args,
- })
- }
- // if the delimiter is null, then the value of the whole
expression is null.
- None => Ok(Expr::Literal(ScalarValue::Utf8(None))),
- }
- }
- Expr::Literal(d) => Err(DataFusionError::Internal(format!(
- "The scalar {} should be casted to string type during the type
coercion.",
- d
- ))),
- d => Ok(concat_ws(
- d.clone(),
- args.iter()
- .cloned()
- .filter(|x| !is_null(x))
- .collect::<Vec<Expr>>(),
- )),
- }
+/// This structure handles API for expression simplification
+pub struct ExprSimplifier<S> {
+ info: S,
}
-impl OptimizerRule for SimplifyExpressions {
- fn name(&self) -> &str {
- "simplify_expressions"
+impl<S: SimplifyInfo> ExprSimplifier<S> {
+ /// Create a new `ExprSimplifier` with the given `info` such as an
+ /// instance of [`SimplifyContext`]. See
+ /// [`simplify`](Self::simplify) for an example.
+ pub fn new(info: S) -> Self {
+ Self { info }
}
- fn optimize(
- &self,
- plan: &LogicalPlan,
- optimizer_config: &mut OptimizerConfig,
- ) -> Result<LogicalPlan> {
- let mut execution_props = ExecutionProps::new();
- execution_props.query_execution_start_time =
- optimizer_config.query_execution_start_time();
- self.optimize_internal(plan, &execution_props)
+ /// Simplifies this [`Expr`]`s as much as possible, evaluating
+ /// constants and applying algebraic simplifications.
+ ///
+ /// The types of the expression must match what operators expect,
+ /// or else an error may occur trying to evaluate. See
+ /// [`coerce`](Self::coerce) for a function to help.
+ ///
+ /// # Example:
+ ///
+ /// `b > 2 AND b > 2`
+ ///
+ /// can be written to
+ ///
+ /// `b > 2`
+ ///
+ /// ```
+ /// use datafusion_expr::{col, lit, Expr};
+ /// use datafusion_common::Result;
+ /// use datafusion_physical_expr::execution_props::ExecutionProps;
+ /// use datafusion_optimizer::simplify_expressions::{ExprSimplifier,
SimplifyInfo};
+ ///
+ /// /// Simple implementation that provides `Simplifier` the information
it needs
+ /// /// See SimplifyContext for a structure that does this.
+ /// #[derive(Default)]
+ /// struct Info {
+ /// execution_props: ExecutionProps,
+ /// };
+ ///
+ /// impl SimplifyInfo for Info {
+ /// fn is_boolean_type(&self, expr: &Expr) -> Result<bool> {
+ /// Ok(false)
+ /// }
+ /// fn nullable(&self, expr: &Expr) -> Result<bool> {
+ /// Ok(true)
+ /// }
+ /// fn execution_props(&self) -> &ExecutionProps {
+ /// &self.execution_props
+ /// }
+ /// }
+ ///
+ /// // Create the simplifier
+ /// let simplifier = ExprSimplifier::new(Info::default());
+ ///
+ /// // b < 2
+ /// let b_lt_2 = col("b").gt(lit(2));
+ ///
+ /// // (b < 2) OR (b < 2)
+ /// let expr = b_lt_2.clone().or(b_lt_2.clone());
+ ///
+ /// // (b < 2) OR (b < 2) --> (b < 2)
+ /// let expr = simplifier.simplify(expr).unwrap();
+ /// assert_eq!(expr, b_lt_2);
+ /// ```
+ pub fn simplify(&self, expr: Expr) -> Result<Expr> {
+ let mut simplifier = Simplifier::new(&self.info);
+ let mut const_evaluator =
ConstEvaluator::try_new(self.info.execution_props())?;
+
+ // TODO iterate until no changes are made during rewrite
+ // (evaluating constants can enable new simplifications and
+ // simplifications can enable new constant evaluation)
+ // https://github.com/apache/arrow-datafusion/issues/1160
+ expr.rewrite(&mut const_evaluator)?
+ .rewrite(&mut simplifier)?
+ // run both passes twice to try an minimize simplifications that
we missed
+ .rewrite(&mut const_evaluator)?
+ .rewrite(&mut simplifier)
}
-}
-
-impl SimplifyExpressions {
- fn optimize_internal(
- &self,
- plan: &LogicalPlan,
- execution_props: &ExecutionProps,
- ) -> Result<LogicalPlan> {
- // We need to pass down the all schemas within the plan tree to
`optimize_expr` in order to
- // to evaluate expression types. For example, a projection plan's
schema will only include
- // projected columns. With just the projected schema, it's not
possible to infer types for
- // expressions that references non-projected columns within the same
project plan or its
- // children plans.
- let info = plan
- .all_schemas()
- .into_iter()
- .fold(SimplifyContext::new(execution_props), |context, schema| {
- context.with_schema(schema.clone())
- });
-
- let simplifier = ExprSimplifier::new(info);
-
- let new_inputs = plan
- .inputs()
- .iter()
- .map(|input| self.optimize_internal(input, execution_props))
- .collect::<Result<Vec<_>>>()?;
-
- let expr = plan
- .expressions()
- .into_iter()
- .map(|e| {
- // We need to keep original expression name, if any.
- // Constant folding should not change expression name.
- let name = &e.display_name();
-
- // Apply the actual simplification logic
- let new_e = simplifier.simplify(e)?;
-
- let new_name = &new_e.display_name();
-
- if let (Ok(expr_name), Ok(new_expr_name)) = (name, new_name) {
- if expr_name != new_expr_name {
- Ok(new_e.alias(expr_name))
- } else {
- Ok(new_e)
- }
- } else {
- Ok(new_e)
- }
- })
- .collect::<Result<Vec<_>>>()?;
- from_plan(plan, &expr, &new_inputs)
- }
-}
+ /// Apply type coercion to an [`Expr`] so that it can be
+ /// evaluated as a
[`PhysicalExpr`](datafusion_physical_expr::PhysicalExpr).
+ ///
+ /// See the [type coercion module](datafusion_expr::type_coercion)
+ /// documentation for more details on type coercion
+ ///
+ // Would be nice if this API could use the SimplifyInfo
+ // rather than creating an DFSchemaRef coerces rather than doing
+ // it manually.
+ // https://github.com/apache/arrow-datafusion/issues/3793
+ pub fn coerce(&self, expr: Expr, schema: DFSchemaRef) -> Result<Expr> {
+ let mut expr_rewrite = TypeCoercionRewriter { schema };
-impl SimplifyExpressions {
- #[allow(missing_docs)]
- pub fn new() -> Self {
- Self {}
+ expr.rewrite(&mut expr_rewrite)
}
}
@@ -459,24 +138,7 @@ impl SimplifyExpressions {
///
/// Note it does not handle algebraic rewrites such as `(a or false)`
/// --> `a`, which is handled by [`Simplifier`]
-///
-/// ```
-/// # use datafusion_expr::{col, lit};
-/// # use datafusion_optimizer::simplify_expressions::ConstEvaluator;
-/// # use datafusion_physical_expr::execution_props::ExecutionProps;
-/// # use datafusion_expr::expr_rewriter::ExprRewritable;
-///
-/// let execution_props = ExecutionProps::new();
-/// let mut const_evaluator =
ConstEvaluator::try_new(&execution_props).unwrap();
-///
-/// // (1 + 2) + a
-/// let expr = (lit(1) + lit(2)) + col("a");
-///
-/// // is rewritten to (3 + a);
-/// let rewritten = expr.rewrite(&mut const_evaluator).unwrap();
-/// assert_eq!(rewritten, lit(3) + col("a"));
-/// ```
-pub struct ConstEvaluator<'a> {
+struct ConstEvaluator<'a> {
/// can_evaluate is used during the depth-first-search of the
/// Expr tree to track if any siblings (or their descendants) were
/// non evaluatable (e.g. had a column reference or volatile
@@ -654,7 +316,7 @@ impl<'a> ConstEvaluator<'a> {
/// * `false = true` and `true = false` to `false`
/// * `!!expr` to `expr`
/// * `expr = null` and `expr != null` to `null`
-pub(crate) struct Simplifier<'a, S> {
+struct Simplifier<'a, S> {
info: &'a S,
}
@@ -667,7 +329,7 @@ impl<'a, S> Simplifier<'a, S> {
impl<'a, S: SimplifyInfo> ExprRewriter for Simplifier<'a, S> {
/// rewrite the expression simplifying any constant expressions
fn mutate(&mut self, expr: Expr) -> Result<Expr> {
- use Operator::{And, Divide, Eq, Modulo, Multiply, NotEq, Or};
+ use datafusion_expr::Operator::{And, Divide, Eq, Modulo, Multiply,
NotEq, Or};
let info = self.info;
let new_expr = match expr {
@@ -1050,194 +712,453 @@ impl<'a, S: SimplifyInfo> ExprRewriter for
Simplifier<'a, S> {
}
}
-/// A macro to assert that one string is contained within another with
-/// a nice error message if they are not.
-///
-/// Usage: `assert_contains!(actual, expected)`
-///
-/// Is a macro so test error
-/// messages are on the same line as the failure;
-///
-/// Both arguments must be convertable into Strings (Into<String>)
-#[macro_export]
-macro_rules! assert_contains {
- ($ACTUAL: expr, $EXPECTED: expr) => {
- let actual_value: String = $ACTUAL.into();
- let expected_value: String = $EXPECTED.into();
- assert!(
- actual_value.contains(&expected_value),
- "Can not find expected in actual.\n\nExpected:\n{}\n\nActual:\n{}",
- expected_value,
- actual_value
- );
- };
-}
-
#[cfg(test)]
mod tests {
+ use std::{collections::HashMap, sync::Arc};
+
+ use crate::simplify_expressions::{
+ utils::for_test::{cast_to_int64_expr, now_expr, to_timestamp_expr},
+ SimplifyContext,
+ };
+
use super::*;
- use arrow::array::{ArrayRef, Int32Array};
+ use arrow::{
+ array::{ArrayRef, Int32Array},
+ datatypes::{DataType, Field, Schema},
+ };
use chrono::{DateTime, TimeZone, Utc};
- use datafusion_common::{DFField, DFSchemaRef};
- use datafusion_expr::expr::{Case, Cast};
- use datafusion_expr::expr_fn::{concat, concat_ws};
- use datafusion_expr::logical_plan::table_scan;
- use datafusion_expr::{
- and, binary_expr, call_fn, col, create_udf, lit, lit_timestamp_nano,
- logical_plan::builder::LogicalPlanBuilder, BuiltinScalarFunction, Expr,
- ExprSchemable, ScalarUDF,
+ use datafusion_common::{DFField, ToDFSchema};
+ use datafusion_expr::*;
+ use datafusion_physical_expr::{
+ execution_props::ExecutionProps, functions::make_scalar_function,
};
- use datafusion_physical_expr::functions::make_scalar_function;
- use std::collections::HashMap;
- use std::sync::Arc;
+ // ------------------------------
+ // --- ExprSimplifier tests -----
+ // ------------------------------
#[test]
- fn test_simplify_or_true() {
- let expr_a = col("c2").or(lit(true));
- let expr_b = lit(true).or(col("c2"));
- let expected = lit(true);
+ fn api_basic() {
+ let props = ExecutionProps::new();
+ let simplifier =
+
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(test_schema()));
- assert_eq!(simplify(expr_a), expected);
- assert_eq!(simplify(expr_b), expected);
+ let expr = lit(1) + lit(2);
+ let expected = lit(3);
+ assert_eq!(expected, simplifier.simplify(expr).unwrap());
}
#[test]
- fn test_simplify_or_false() {
- let expr_a = lit(false).or(col("c2"));
- let expr_b = col("c2").or(lit(false));
- let expected = col("c2");
+ fn basic_coercion() {
+ let schema = test_schema();
+ let props = ExecutionProps::new();
+ let simplifier =
+
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(schema.clone()));
- assert_eq!(simplify(expr_a), expected);
- assert_eq!(simplify(expr_b), expected);
- }
+ // Note expr type is int32 (not int64)
+ // (1i64 + 2i32) < i
+ let expr = (lit(1i64) + lit(2i32)).lt(col("i"));
+ // should fully simplify to 3 < i (though i has been coerced to i64)
+ let expected = lit(3i64).lt(col("i"));
- #[test]
- fn test_simplify_or_same() {
- let expr = col("c2").or(col("c2"));
- let expected = col("c2");
+ // Would be nice if this API could use the SimplifyInfo
+ // rather than creating an DFSchemaRef coerces rather than doing
+ // it manually.
+ // https://github.com/apache/arrow-datafusion/issues/3793
+ let expr = simplifier.coerce(expr, schema).unwrap();
- assert_eq!(simplify(expr), expected);
+ assert_eq!(expected, simplifier.simplify(expr).unwrap());
}
- #[test]
- fn test_simplify_and_false() {
- let expr_a = lit(false).and(col("c2"));
- let expr_b = col("c2").and(lit(false));
- let expected = lit(false);
-
- assert_eq!(simplify(expr_a), expected);
- assert_eq!(simplify(expr_b), expected);
+ fn test_schema() -> DFSchemaRef {
+ Schema::new(vec![
+ Field::new("i", DataType::Int64, false),
+ Field::new("b", DataType::Boolean, true),
+ ])
+ .to_dfschema_ref()
+ .unwrap()
}
#[test]
- fn test_simplify_and_same() {
- let expr = col("c2").and(col("c2"));
- let expected = col("c2");
+ fn simplify_and_constant_prop() {
+ let props = ExecutionProps::new();
+ let simplifier =
+
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(test_schema()));
- assert_eq!(simplify(expr), expected);
+ // should be able to simplify to false
+ // (i * (1 - 2)) > 0
+ let expr = (col("i") * (lit(1) - lit(1))).gt(lit(0));
+ let expected = lit(false);
+ assert_eq!(expected, simplifier.simplify(expr).unwrap());
}
#[test]
- fn test_simplify_and_true() {
- let expr_a = lit(true).and(col("c2"));
- let expr_b = col("c2").and(lit(true));
- let expected = col("c2");
+ fn simplify_and_constant_prop_with_case() {
+ let props = ExecutionProps::new();
+ let simplifier =
+
ExprSimplifier::new(SimplifyContext::new(&props).with_schema(test_schema()));
- assert_eq!(simplify(expr_a), expected);
- assert_eq!(simplify(expr_b), expected);
+ // CASE
+ // WHEN i>5 AND false THEN i > 5
+ // WHEN i<5 AND true THEN i < 5
+ // ELSE false
+ // END
+ //
+ // Can be simplified to `i < 5`
+ let expr = when(col("i").gt(lit(5)).and(lit(false)),
col("i").gt(lit(5)))
+ .when(col("i").lt(lit(5)).and(lit(true)), col("i").lt(lit(5)))
+ .otherwise(lit(false))
+ .unwrap();
+ let expected = col("i").lt(lit(5));
+ assert_eq!(expected, simplifier.simplify(expr).unwrap());
}
- #[test]
- fn test_simplify_multiply_by_one() {
- let expr_a = binary_expr(col("c2"), Operator::Multiply, lit(1));
- let expr_b = binary_expr(lit(1), Operator::Multiply, col("c2"));
- let expected = col("c2");
+ // ------------------------------
+ // --- ConstEvaluator tests -----
+ // ------------------------------
+ fn test_evaluate_with_start_time(
+ input_expr: Expr,
+ expected_expr: Expr,
+ date_time: &DateTime<Utc>,
+ ) {
+ let execution_props = ExecutionProps {
+ query_execution_start_time: *date_time,
+ var_providers: None,
+ };
- assert_eq!(simplify(expr_a), expected);
- assert_eq!(simplify(expr_b), expected);
+ let mut const_evaluator =
ConstEvaluator::try_new(&execution_props).unwrap();
+ let evaluated_expr = input_expr
+ .clone()
+ .rewrite(&mut const_evaluator)
+ .expect("successfully evaluated");
- let expr = binary_expr(
- col("c2"),
- Operator::Multiply,
- Expr::Literal(ScalarValue::Decimal128(Some(10000000000), 38, 10)),
- );
- assert_eq!(simplify(expr), expected);
- let expr = binary_expr(
- Expr::Literal(ScalarValue::Decimal128(Some(10000000000), 31, 10)),
- Operator::Multiply,
- col("c2"),
+ assert_eq!(
+ evaluated_expr, expected_expr,
+ "Mismatch evaluating {}\n Expected:{}\n Got:{}",
+ input_expr, expected_expr, evaluated_expr
);
- assert_eq!(simplify(expr), expected);
}
- #[test]
- fn test_simplify_multiply_by_null() {
- let null = Expr::Literal(ScalarValue::Null);
- // A * null --> null
- {
- let expr = binary_expr(col("c2"), Operator::Multiply,
null.clone());
- assert_eq!(simplify(expr), null);
- }
- // null * A --> null
- {
- let expr = binary_expr(null.clone(), Operator::Multiply,
col("c2"));
- assert_eq!(simplify(expr), null);
- }
+ fn test_evaluate(input_expr: Expr, expected_expr: Expr) {
+ test_evaluate_with_start_time(input_expr, expected_expr, &Utc::now())
}
- #[test]
- fn test_simplify_multiply_by_zero() {
- // cannot optimize A * null (null * A) if A is nullable
- {
- let expr_a = binary_expr(col("c2"), Operator::Multiply, lit(0));
- let expr_b = binary_expr(lit(0), Operator::Multiply, col("c2"));
+ // Make a UDF that adds its two values together, with the specified
volatility
+ fn make_udf_add(volatility: Volatility) -> Arc<ScalarUDF> {
+ let input_types = vec![DataType::Int32, DataType::Int32];
+ let return_type = Arc::new(DataType::Int32);
- assert_eq!(simplify(expr_a.clone()), expr_a);
- assert_eq!(simplify(expr_b.clone()), expr_b);
- }
- // 0 * A --> 0 if A is not nullable
- {
- let expr = binary_expr(lit(0), Operator::Multiply,
col("c2_non_null"));
- assert_eq!(simplify(expr), lit(0));
- }
- // A * 0 --> 0 if A is not nullable
- {
- let expr = binary_expr(col("c2_non_null"), Operator::Multiply,
lit(0));
- assert_eq!(simplify(expr), lit(0));
- }
- // A * Decimal128(0) --> 0 if A is not nullable
- {
- let expr = binary_expr(
- col("c2_non_null"),
- Operator::Multiply,
- Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10)),
- );
- assert_eq!(
- simplify(expr),
- Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10))
- );
- let expr = binary_expr(
- Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10)),
- Operator::Multiply,
- col("c2_non_null"),
- );
- assert_eq!(
- simplify(expr),
- Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10))
- );
- }
- }
+ let fun = |args: &[ArrayRef]| {
+ let arg0 = &args[0]
+ .as_any()
+ .downcast_ref::<Int32Array>()
+ .expect("cast failed");
+ let arg1 = &args[1]
+ .as_any()
+ .downcast_ref::<Int32Array>()
+ .expect("cast failed");
- #[test]
- fn test_simplify_divide_by_one() {
- let expr = binary_expr(col("c2"), Operator::Divide, lit(1));
- let expected = col("c2");
- assert_eq!(simplify(expr), expected);
- let expr = binary_expr(
- col("c2"),
- Operator::Divide,
- Expr::Literal(ScalarValue::Decimal128(Some(10000000000), 31, 10)),
- );
+ // 2. perform the computation
+ let array = arg0
+ .iter()
+ .zip(arg1.iter())
+ .map(|args| {
+ if let (Some(arg0), Some(arg1)) = args {
+ Some(arg0 + arg1)
+ } else {
+ // one or both args were Null
+ None
+ }
+ })
+ .collect::<Int32Array>();
+
+ Ok(Arc::new(array) as ArrayRef)
+ };
+
+ let fun = make_scalar_function(fun);
+ Arc::new(create_udf(
+ "udf_add",
+ input_types,
+ return_type,
+ volatility,
+ fun,
+ ))
+ }
+
+ #[test]
+ fn test_const_evaluator() {
+ // true --> true
+ test_evaluate(lit(true), lit(true));
+ // true or true --> true
+ test_evaluate(lit(true).or(lit(true)), lit(true));
+ // true or false --> true
+ test_evaluate(lit(true).or(lit(false)), lit(true));
+
+ // "foo" == "foo" --> true
+ test_evaluate(lit("foo").eq(lit("foo")), lit(true));
+ // "foo" != "foo" --> false
+ test_evaluate(lit("foo").not_eq(lit("foo")), lit(false));
+
+ // c = 1 --> c = 1
+ test_evaluate(col("c").eq(lit(1)), col("c").eq(lit(1)));
+ // c = 1 + 2 --> c + 3
+ test_evaluate(col("c").eq(lit(1) + lit(2)), col("c").eq(lit(3)));
+ // (foo != foo) OR (c = 1) --> false OR (c = 1)
+ test_evaluate(
+ (lit("foo").not_eq(lit("foo"))).or(col("c").eq(lit(1))),
+ lit(false).or(col("c").eq(lit(1))),
+ );
+ }
+
+ #[test]
+ fn test_const_evaluator_scalar_functions() {
+ // concat("foo", "bar") --> "foobar"
+ let expr = call_fn("concat", vec![lit("foo"), lit("bar")]).unwrap();
+ test_evaluate(expr, lit("foobar"));
+
+ // ensure arguments are also constant folded
+ // concat("foo", concat("bar", "baz")) --> "foobarbaz"
+ let concat1 = call_fn("concat", vec![lit("bar"), lit("baz")]).unwrap();
+ let expr = call_fn("concat", vec![lit("foo"), concat1]).unwrap();
+ test_evaluate(expr, lit("foobarbaz"));
+
+ // Check non string arguments
+ // to_timestamp("2020-09-08T12:00:00+00:00") -->
timestamp(1599566400000000000i64)
+ let expr =
+ call_fn("to_timestamp",
vec![lit("2020-09-08T12:00:00+00:00")]).unwrap();
+ test_evaluate(expr, lit_timestamp_nano(1599566400000000000i64));
+
+ // check that non foldable arguments are folded
+ // to_timestamp(a) --> to_timestamp(a) [no rewrite possible]
+ let expr = call_fn("to_timestamp", vec![col("a")]).unwrap();
+ test_evaluate(expr.clone(), expr);
+
+ // check that non foldable arguments are folded
+ // to_timestamp(a) --> to_timestamp(a) [no rewrite possible]
+ let expr = call_fn("to_timestamp", vec![col("a")]).unwrap();
+ test_evaluate(expr.clone(), expr);
+
+ // volatile / stable functions should not be evaluated
+ // rand() + (1 + 2) --> rand() + 3
+ let fun = BuiltinScalarFunction::Random;
+ assert_eq!(fun.volatility(), Volatility::Volatile);
+ let rand = Expr::ScalarFunction { args: vec![], fun };
+ let expr = rand.clone() + (lit(1) + lit(2));
+ let expected = rand + lit(3);
+ test_evaluate(expr, expected);
+
+ // parenthesization matters: can't rewrite
+ // (rand() + 1) + 2 --> (rand() + 1) + 2)
+ let fun = BuiltinScalarFunction::Random;
+ let rand = Expr::ScalarFunction { args: vec![], fun };
+ let expr = (rand + lit(1)) + lit(2);
+ test_evaluate(expr.clone(), expr);
+ }
+
+ #[test]
+ fn test_const_evaluator_now() {
+ let ts_nanos = 1599566400000000000i64;
+ let time = chrono::Utc.timestamp_nanos(ts_nanos);
+ let ts_string = "2020-09-08T12:05:00+00:00";
+ // now() --> ts
+ test_evaluate_with_start_time(now_expr(),
lit_timestamp_nano(ts_nanos), &time);
+
+ // CAST(now() as int64) + 100_i64 --> ts + 100_i64
+ let expr = cast_to_int64_expr(now_expr()) + lit(100_i64);
+ test_evaluate_with_start_time(expr, lit(ts_nanos + 100), &time);
+
+ // CAST(now() as int64) < cast(to_timestamp(...) as int64) +
50000_i64 ---> true
+ let expr = cast_to_int64_expr(now_expr())
+ .lt(cast_to_int64_expr(to_timestamp_expr(ts_string)) +
lit(50000i64));
+ test_evaluate_with_start_time(expr, lit(true), &time);
+ }
+
+ #[test]
+ fn test_evaluator_udfs() {
+ let args = vec![lit(1) + lit(2), lit(30) + lit(40)];
+ let folded_args = vec![lit(3), lit(70)];
+
+ // immutable UDF should get folded
+ // udf_add(1+2, 30+40) --> 73
+ let expr = Expr::ScalarUDF {
+ args: args.clone(),
+ fun: make_udf_add(Volatility::Immutable),
+ };
+ test_evaluate(expr, lit(73));
+
+ // stable UDF should be entirely folded
+ // udf_add(1+2, 30+40) --> 73
+ let fun = make_udf_add(Volatility::Stable);
+ let expr = Expr::ScalarUDF {
+ args: args.clone(),
+ fun: Arc::clone(&fun),
+ };
+ test_evaluate(expr, lit(73));
+
+ // volatile UDF should have args folded
+ // udf_add(1+2, 30+40) --> udf_add(3, 70)
+ let fun = make_udf_add(Volatility::Volatile);
+ let expr = Expr::ScalarUDF {
+ args,
+ fun: Arc::clone(&fun),
+ };
+ let expected_expr = Expr::ScalarUDF {
+ args: folded_args,
+ fun: Arc::clone(&fun),
+ };
+ test_evaluate(expr, expected_expr);
+ }
+
+ // ------------------------------
+ // --- Simplifier tests -----
+ // ------------------------------
+
+ #[test]
+ fn test_simplify_or_true() {
+ let expr_a = col("c2").or(lit(true));
+ let expr_b = lit(true).or(col("c2"));
+ let expected = lit(true);
+
+ assert_eq!(simplify(expr_a), expected);
+ assert_eq!(simplify(expr_b), expected);
+ }
+
+ #[test]
+ fn test_simplify_or_false() {
+ let expr_a = lit(false).or(col("c2"));
+ let expr_b = col("c2").or(lit(false));
+ let expected = col("c2");
+
+ assert_eq!(simplify(expr_a), expected);
+ assert_eq!(simplify(expr_b), expected);
+ }
+
+ #[test]
+ fn test_simplify_or_same() {
+ let expr = col("c2").or(col("c2"));
+ let expected = col("c2");
+
+ assert_eq!(simplify(expr), expected);
+ }
+
+ #[test]
+ fn test_simplify_and_false() {
+ let expr_a = lit(false).and(col("c2"));
+ let expr_b = col("c2").and(lit(false));
+ let expected = lit(false);
+
+ assert_eq!(simplify(expr_a), expected);
+ assert_eq!(simplify(expr_b), expected);
+ }
+
+ #[test]
+ fn test_simplify_and_same() {
+ let expr = col("c2").and(col("c2"));
+ let expected = col("c2");
+
+ assert_eq!(simplify(expr), expected);
+ }
+
+ #[test]
+ fn test_simplify_and_true() {
+ let expr_a = lit(true).and(col("c2"));
+ let expr_b = col("c2").and(lit(true));
+ let expected = col("c2");
+
+ assert_eq!(simplify(expr_a), expected);
+ assert_eq!(simplify(expr_b), expected);
+ }
+
+ #[test]
+ fn test_simplify_multiply_by_one() {
+ let expr_a = binary_expr(col("c2"), Operator::Multiply, lit(1));
+ let expr_b = binary_expr(lit(1), Operator::Multiply, col("c2"));
+ let expected = col("c2");
+
+ assert_eq!(simplify(expr_a), expected);
+ assert_eq!(simplify(expr_b), expected);
+
+ let expr = binary_expr(
+ col("c2"),
+ Operator::Multiply,
+ Expr::Literal(ScalarValue::Decimal128(Some(10000000000), 38, 10)),
+ );
+ assert_eq!(simplify(expr), expected);
+ let expr = binary_expr(
+ Expr::Literal(ScalarValue::Decimal128(Some(10000000000), 31, 10)),
+ Operator::Multiply,
+ col("c2"),
+ );
+ assert_eq!(simplify(expr), expected);
+ }
+
+ #[test]
+ fn test_simplify_multiply_by_null() {
+ let null = Expr::Literal(ScalarValue::Null);
+ // A * null --> null
+ {
+ let expr = binary_expr(col("c2"), Operator::Multiply,
null.clone());
+ assert_eq!(simplify(expr), null);
+ }
+ // null * A --> null
+ {
+ let expr = binary_expr(null.clone(), Operator::Multiply,
col("c2"));
+ assert_eq!(simplify(expr), null);
+ }
+ }
+
+ #[test]
+ fn test_simplify_multiply_by_zero() {
+ // cannot optimize A * null (null * A) if A is nullable
+ {
+ let expr_a = binary_expr(col("c2"), Operator::Multiply, lit(0));
+ let expr_b = binary_expr(lit(0), Operator::Multiply, col("c2"));
+
+ assert_eq!(simplify(expr_a.clone()), expr_a);
+ assert_eq!(simplify(expr_b.clone()), expr_b);
+ }
+ // 0 * A --> 0 if A is not nullable
+ {
+ let expr = binary_expr(lit(0), Operator::Multiply,
col("c2_non_null"));
+ assert_eq!(simplify(expr), lit(0));
+ }
+ // A * 0 --> 0 if A is not nullable
+ {
+ let expr = binary_expr(col("c2_non_null"), Operator::Multiply,
lit(0));
+ assert_eq!(simplify(expr), lit(0));
+ }
+ // A * Decimal128(0) --> 0 if A is not nullable
+ {
+ let expr = binary_expr(
+ col("c2_non_null"),
+ Operator::Multiply,
+ Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10)),
+ );
+ assert_eq!(
+ simplify(expr),
+ Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10))
+ );
+ let expr = binary_expr(
+ Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10)),
+ Operator::Multiply,
+ col("c2_non_null"),
+ );
+ assert_eq!(
+ simplify(expr),
+ Expr::Literal(ScalarValue::Decimal128(Some(0), 31, 10))
+ );
+ }
+ }
+
+ #[test]
+ fn test_simplify_divide_by_one() {
+ let expr = binary_expr(col("c2"), Operator::Divide, lit(1));
+ let expected = col("c2");
+ assert_eq!(simplify(expr), expected);
+ let expr = binary_expr(
+ col("c2"),
+ Operator::Divide,
+ Expr::Literal(ScalarValue::Decimal128(Some(10000000000), 31, 10)),
+ );
assert_eq!(simplify(expr), expected);
}
@@ -1538,241 +1459,32 @@ mod tests {
{
let sub_expr = concat_ws(null.clone(), vec![col("c1"), col("c2")]);
let expr = concat_ws(lit("|"), vec![sub_expr, col("c3")]);
- assert_eq!(simplify(expr), concat_ws(lit("|"), vec![col("c3")]));
- }
-
- // null delimiter (nested)
- {
- let sub_expr = concat_ws(null.clone(), vec![col("c1"), col("c2")]);
- let expr = concat_ws(sub_expr, vec![col("c3"), col("c4")]);
- assert_eq!(simplify(expr), null);
- }
- }
-
- #[test]
- fn test_simplify_concat() {
- let null = Expr::Literal(ScalarValue::Utf8(None));
- let expr = concat(&[
- null.clone(),
- col("c0"),
- lit("hello "),
- null.clone(),
- lit("rust"),
- col("c1"),
- lit(""),
- null,
- ]);
- let expected = concat(&[col("c0"), lit("hello rust"), col("c1")]);
- assert_eq!(simplify(expr), expected)
- }
-
- // ------------------------------
- // --- ConstEvaluator tests -----
- // ------------------------------
-
- #[test]
- fn test_const_evaluator() {
- // true --> true
- test_evaluate(lit(true), lit(true));
- // true or true --> true
- test_evaluate(lit(true).or(lit(true)), lit(true));
- // true or false --> true
- test_evaluate(lit(true).or(lit(false)), lit(true));
-
- // "foo" == "foo" --> true
- test_evaluate(lit("foo").eq(lit("foo")), lit(true));
- // "foo" != "foo" --> false
- test_evaluate(lit("foo").not_eq(lit("foo")), lit(false));
-
- // c = 1 --> c = 1
- test_evaluate(col("c").eq(lit(1)), col("c").eq(lit(1)));
- // c = 1 + 2 --> c + 3
- test_evaluate(col("c").eq(lit(1) + lit(2)), col("c").eq(lit(3)));
- // (foo != foo) OR (c = 1) --> false OR (c = 1)
- test_evaluate(
- (lit("foo").not_eq(lit("foo"))).or(col("c").eq(lit(1))),
- lit(false).or(col("c").eq(lit(1))),
- );
- }
-
- #[test]
- fn test_const_evaluator_scalar_functions() {
- // concat("foo", "bar") --> "foobar"
- let expr = call_fn("concat", vec![lit("foo"), lit("bar")]).unwrap();
- test_evaluate(expr, lit("foobar"));
-
- // ensure arguments are also constant folded
- // concat("foo", concat("bar", "baz")) --> "foobarbaz"
- let concat1 = call_fn("concat", vec![lit("bar"), lit("baz")]).unwrap();
- let expr = call_fn("concat", vec![lit("foo"), concat1]).unwrap();
- test_evaluate(expr, lit("foobarbaz"));
-
- // Check non string arguments
- // to_timestamp("2020-09-08T12:00:00+00:00") -->
timestamp(1599566400000000000i64)
- let expr =
- call_fn("to_timestamp",
vec![lit("2020-09-08T12:00:00+00:00")]).unwrap();
- test_evaluate(expr, lit_timestamp_nano(1599566400000000000i64));
-
- // check that non foldable arguments are folded
- // to_timestamp(a) --> to_timestamp(a) [no rewrite possible]
- let expr = call_fn("to_timestamp", vec![col("a")]).unwrap();
- test_evaluate(expr.clone(), expr);
-
- // check that non foldable arguments are folded
- // to_timestamp(a) --> to_timestamp(a) [no rewrite possible]
- let expr = call_fn("to_timestamp", vec![col("a")]).unwrap();
- test_evaluate(expr.clone(), expr);
-
- // volatile / stable functions should not be evaluated
- // rand() + (1 + 2) --> rand() + 3
- let fun = BuiltinScalarFunction::Random;
- assert_eq!(fun.volatility(), Volatility::Volatile);
- let rand = Expr::ScalarFunction { args: vec![], fun };
- let expr = rand.clone() + (lit(1) + lit(2));
- let expected = rand + lit(3);
- test_evaluate(expr, expected);
-
- // parenthesization matters: can't rewrite
- // (rand() + 1) + 2 --> (rand() + 1) + 2)
- let fun = BuiltinScalarFunction::Random;
- let rand = Expr::ScalarFunction { args: vec![], fun };
- let expr = (rand + lit(1)) + lit(2);
- test_evaluate(expr.clone(), expr);
- }
-
- #[test]
- fn test_const_evaluator_now() {
- let ts_nanos = 1599566400000000000i64;
- let time = chrono::Utc.timestamp_nanos(ts_nanos);
- let ts_string = "2020-09-08T12:05:00+00:00";
- // now() --> ts
- test_evaluate_with_start_time(now_expr(),
lit_timestamp_nano(ts_nanos), &time);
-
- // CAST(now() as int64) + 100_i64 --> ts + 100_i64
- let expr = cast_to_int64_expr(now_expr()) + lit(100_i64);
- test_evaluate_with_start_time(expr, lit(ts_nanos + 100), &time);
-
- // CAST(now() as int64) < cast(to_timestamp(...) as int64) +
50000_i64 ---> true
- let expr = cast_to_int64_expr(now_expr())
- .lt(cast_to_int64_expr(to_timestamp_expr(ts_string)) +
lit(50000i64));
- test_evaluate_with_start_time(expr, lit(true), &time);
- }
-
- fn now_expr() -> Expr {
- call_fn("now", vec![]).unwrap()
- }
-
- fn cast_to_int64_expr(expr: Expr) -> Expr {
- Expr::Cast(Cast::new(expr.into(), DataType::Int64))
- }
-
- fn to_timestamp_expr(arg: impl Into<String>) -> Expr {
- call_fn("to_timestamp", vec![lit(arg.into())]).unwrap()
- }
-
- #[test]
- fn test_evaluator_udfs() {
- let args = vec![lit(1) + lit(2), lit(30) + lit(40)];
- let folded_args = vec![lit(3), lit(70)];
-
- // immutable UDF should get folded
- // udf_add(1+2, 30+40) --> 73
- let expr = Expr::ScalarUDF {
- args: args.clone(),
- fun: make_udf_add(Volatility::Immutable),
- };
- test_evaluate(expr, lit(73));
-
- // stable UDF should be entirely folded
- // udf_add(1+2, 30+40) --> 73
- let fun = make_udf_add(Volatility::Stable);
- let expr = Expr::ScalarUDF {
- args: args.clone(),
- fun: Arc::clone(&fun),
- };
- test_evaluate(expr, lit(73));
-
- // volatile UDF should have args folded
- // udf_add(1+2, 30+40) --> udf_add(3, 70)
- let fun = make_udf_add(Volatility::Volatile);
- let expr = Expr::ScalarUDF {
- args,
- fun: Arc::clone(&fun),
- };
- let expected_expr = Expr::ScalarUDF {
- args: folded_args,
- fun: Arc::clone(&fun),
- };
- test_evaluate(expr, expected_expr);
- }
-
- // Make a UDF that adds its two values together, with the specified
volatility
- fn make_udf_add(volatility: Volatility) -> Arc<ScalarUDF> {
- let input_types = vec![DataType::Int32, DataType::Int32];
- let return_type = Arc::new(DataType::Int32);
-
- let fun = |args: &[ArrayRef]| {
- let arg0 = &args[0]
- .as_any()
- .downcast_ref::<Int32Array>()
- .expect("cast failed");
- let arg1 = &args[1]
- .as_any()
- .downcast_ref::<Int32Array>()
- .expect("cast failed");
-
- // 2. perform the computation
- let array = arg0
- .iter()
- .zip(arg1.iter())
- .map(|args| {
- if let (Some(arg0), Some(arg1)) = args {
- Some(arg0 + arg1)
- } else {
- // one or both args were Null
- None
- }
- })
- .collect::<Int32Array>();
-
- Ok(Arc::new(array) as ArrayRef)
- };
-
- let fun = make_scalar_function(fun);
- Arc::new(create_udf(
- "udf_add",
- input_types,
- return_type,
- volatility,
- fun,
- ))
- }
-
- fn test_evaluate_with_start_time(
- input_expr: Expr,
- expected_expr: Expr,
- date_time: &DateTime<Utc>,
- ) {
- let execution_props = ExecutionProps {
- query_execution_start_time: *date_time,
- var_providers: None,
- };
-
- let mut const_evaluator =
ConstEvaluator::try_new(&execution_props).unwrap();
- let evaluated_expr = input_expr
- .clone()
- .rewrite(&mut const_evaluator)
- .expect("successfully evaluated");
-
- assert_eq!(
- evaluated_expr, expected_expr,
- "Mismatch evaluating {}\n Expected:{}\n Got:{}",
- input_expr, expected_expr, evaluated_expr
- );
+ assert_eq!(simplify(expr), concat_ws(lit("|"), vec![col("c3")]));
+ }
+
+ // null delimiter (nested)
+ {
+ let sub_expr = concat_ws(null.clone(), vec![col("c1"), col("c2")]);
+ let expr = concat_ws(sub_expr, vec![col("c3"), col("c4")]);
+ assert_eq!(simplify(expr), null);
+ }
}
- fn test_evaluate(input_expr: Expr, expected_expr: Expr) {
- test_evaluate_with_start_time(input_expr, expected_expr, &Utc::now())
+ #[test]
+ fn test_simplify_concat() {
+ let null = Expr::Literal(ScalarValue::Utf8(None));
+ let expr = concat(&[
+ null.clone(),
+ col("c0"),
+ lit("hello "),
+ null.clone(),
+ lit("rust"),
+ col("c1"),
+ lit(""),
+ null,
+ ]);
+ let expected = concat(&[col("c0"), lit("hello rust"), col("c1")]);
+ assert_eq!(simplify(expr), expected)
}
// ------------------------------
@@ -2101,696 +1813,4 @@ mod tests {
or(col("c2").lt(lit(3)), col("c2").gt(lit(4)))
);
}
-
- // ------------------------------
- // -- SimplifyExpressions tests -
- // (test plans are simplified correctly)
- // ------------------------------
-
- fn test_table_scan() -> LogicalPlan {
- let schema = Schema::new(vec![
- Field::new("a", DataType::Boolean, false),
- Field::new("b", DataType::Boolean, false),
- Field::new("c", DataType::Boolean, false),
- Field::new("d", DataType::UInt32, false),
- ]);
- table_scan(Some("test"), &schema, None)
- .expect("creating scan")
- .build()
- .expect("building plan")
- }
-
- fn assert_optimized_plan_eq(plan: &LogicalPlan, expected: &str) {
- let rule = SimplifyExpressions::new();
- let optimized_plan = rule
- .optimize(plan, &mut OptimizerConfig::new())
- .expect("failed to optimize plan");
- let formatted_plan = format!("{:?}", optimized_plan);
- assert_eq!(formatted_plan, expected);
- }
-
- #[test]
- fn test_simplify_optimized_plan() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(vec![col("a")])
- .unwrap()
- .filter(and(col("b").gt(lit(1)), col("b").gt(lit(1))))
- .unwrap()
- .build()
- .unwrap();
-
- assert_optimized_plan_eq(
- &plan,
- "\
- Filter: test.b > Int32(1)\
- \n Projection: test.a\
- \n TableScan: test",
- );
- }
-
- #[test]
- fn test_simplify_optimized_plan_with_or() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(vec![col("a")])
- .unwrap()
- .filter(or(col("b").gt(lit(1)), col("b").gt(lit(1))))
- .unwrap()
- .build()
- .unwrap();
-
- assert_optimized_plan_eq(
- &plan,
- "\
- Filter: test.b > Int32(1)\
- \n Projection: test.a\
- \n TableScan: test",
- );
- }
-
- #[test]
- fn test_simplify_optimized_plan_with_composed_and() {
- let table_scan = test_table_scan();
- // ((c > 5) AND (d < 6)) AND (c > 5) --> (c > 5) AND (d < 6)
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(vec![col("a"), col("b")])
- .unwrap()
- .filter(and(
- and(col("a").gt(lit(5)), col("b").lt(lit(6))),
- col("a").gt(lit(5)),
- ))
- .unwrap()
- .build()
- .unwrap();
-
- assert_optimized_plan_eq(
- &plan,
- "\
- Filter: test.a > Int32(5) AND test.b < Int32(6)\
- \n Projection: test.a, test.b\
- \n TableScan: test",
- );
- }
-
- #[test]
- fn test_simplity_optimized_plan_eq_expr() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("b").eq(lit(true)))
- .unwrap()
- .filter(col("c").eq(lit(false)))
- .unwrap()
- .project(vec![col("a")])
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "\
- Projection: test.a\
- \n Filter: NOT test.c\
- \n Filter: test.b\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn test_simplity_optimized_plan_not_eq_expr() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("b").not_eq(lit(true)))
- .unwrap()
- .filter(col("c").not_eq(lit(false)))
- .unwrap()
- .limit(0, Some(1))
- .unwrap()
- .project(vec![col("a")])
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "\
- Projection: test.a\
- \n Limit: skip=0, fetch=1\
- \n Filter: test.c\
- \n Filter: NOT test.b\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn test_simplity_optimized_plan_and_expr() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("b").not_eq(lit(true)).and(col("c").eq(lit(true))))
- .unwrap()
- .project(vec![col("a")])
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "\
- Projection: test.a\
- \n Filter: NOT test.b AND test.c\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn test_simplity_optimized_plan_or_expr() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("b").not_eq(lit(true)).or(col("c").eq(lit(false))))
- .unwrap()
- .project(vec![col("a")])
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "\
- Projection: test.a\
- \n Filter: NOT test.b OR NOT test.c\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn test_simplity_optimized_plan_not_expr() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("b").eq(lit(false)).not())
- .unwrap()
- .project(vec![col("a")])
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "\
- Projection: test.a\
- \n Filter: test.b\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn test_simplity_optimized_plan_support_projection() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(vec![col("a"), col("d"), col("b").eq(lit(false))])
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "\
- Projection: test.a, test.d, NOT test.b AS test.b = Boolean(false)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn test_simplity_optimized_plan_support_aggregate() {
- let table_scan = test_table_scan();
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(vec![col("a"), col("c"), col("b")])
- .unwrap()
- .aggregate(
- vec![col("a"), col("c")],
- vec![
- datafusion_expr::max(col("b").eq(lit(true))),
- datafusion_expr::min(col("b")),
- ],
- )
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "\
- Aggregate: groupBy=[[test.a, test.c]], aggr=[[MAX(test.b) AS
MAX(test.b = Boolean(true)), MIN(test.b)]]\
- \n Projection: test.a, test.c, test.b\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn test_simplity_optimized_plan_support_values() {
- let expr1 = Expr::BinaryExpr(BinaryExpr::new(
- Box::new(lit(1)),
- Operator::Plus,
- Box::new(lit(2)),
- ));
- let expr2 = Expr::BinaryExpr(BinaryExpr::new(
- Box::new(lit(2)),
- Operator::Minus,
- Box::new(lit(1)),
- ));
- let values = vec![vec![expr1, expr2]];
- let plan =
LogicalPlanBuilder::values(values).unwrap().build().unwrap();
-
- let expected = "\
- Values: (Int32(3) AS Int32(1) + Int32(2), Int32(1) AS Int32(2) -
Int32(1))";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- // expect optimizing will result in an error, returning the error string
- fn get_optimized_plan_err(plan: &LogicalPlan, date_time: &DateTime<Utc>)
-> String {
- let mut config =
- OptimizerConfig::new().with_query_execution_start_time(*date_time);
- let rule = SimplifyExpressions::new();
-
- let err = rule
- .optimize(plan, &mut config)
- .expect_err("expected optimization to fail");
-
- err.to_string()
- }
-
- fn get_optimized_plan_formatted(
- plan: &LogicalPlan,
- date_time: &DateTime<Utc>,
- ) -> String {
- let mut config =
- OptimizerConfig::new().with_query_execution_start_time(*date_time);
- let rule = SimplifyExpressions::new();
-
- let optimized_plan = rule
- .optimize(plan, &mut config)
- .expect("failed to optimize plan");
- format!("{:?}", optimized_plan)
- }
-
- #[test]
- fn to_timestamp_expr_folded() {
- let table_scan = test_table_scan();
- let proj = vec![to_timestamp_expr("2020-09-08T12:00:00+00:00")];
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(proj)
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "Projection: TimestampNanosecond(1599566400000000000,
None) AS totimestamp(Utf8(\"2020-09-08T12:00:00+00:00\"))\
- \n TableScan: test"
- .to_string();
- let actual = get_optimized_plan_formatted(&plan, &Utc::now());
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn to_timestamp_expr_wrong_arg() {
- let table_scan = test_table_scan();
- let proj = vec![to_timestamp_expr("I'M NOT A TIMESTAMP")];
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(proj)
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "Error parsing 'I'M NOT A TIMESTAMP' as timestamp";
- let actual = get_optimized_plan_err(&plan, &Utc::now());
- assert_contains!(actual, expected);
- }
-
- #[test]
- fn cast_expr() {
- let table_scan = test_table_scan();
- let proj = vec![Expr::Cast(Cast::new(Box::new(lit("0")),
DataType::Int32))];
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(proj)
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "Projection: Int32(0) AS Utf8(\"0\")\
- \n TableScan: test";
- let actual = get_optimized_plan_formatted(&plan, &Utc::now());
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn cast_expr_wrong_arg() {
- let table_scan = test_table_scan();
- let proj = vec![Expr::Cast(Cast::new(Box::new(lit("")),
DataType::Int32))];
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(proj)
- .unwrap()
- .build()
- .unwrap();
-
- let expected = "Cannot cast string '' to value of Int32 type";
- let actual = get_optimized_plan_err(&plan, &Utc::now());
- assert_contains!(actual, expected);
- }
-
- #[test]
- fn multiple_now_expr() {
- let table_scan = test_table_scan();
- let time = Utc::now();
- let proj = vec![
- now_expr(),
- Expr::Alias(Box::new(now_expr()), "t2".to_string()),
- ];
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(proj)
- .unwrap()
- .build()
- .unwrap();
-
- // expect the same timestamp appears in both exprs
- let actual = get_optimized_plan_formatted(&plan, &time);
- let expected = format!(
- "Projection: TimestampNanosecond({}, Some(\"UTC\")) AS now(),
TimestampNanosecond({}, Some(\"UTC\")) AS t2\
- \n TableScan: test",
- time.timestamp_nanos(),
- time.timestamp_nanos()
- );
-
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn simplify_and_eval() {
- // demonstrate a case where the evaluation needs to run prior
- // to the simplifier for it to work
- let table_scan = test_table_scan();
- let time = Utc::now();
- // (true or false) != col --> !col
- let proj = vec![lit(true).or(lit(false)).not_eq(col("a"))];
- let plan = LogicalPlanBuilder::from(table_scan)
- .project(proj)
- .unwrap()
- .build()
- .unwrap();
-
- let actual = get_optimized_plan_formatted(&plan, &time);
- let expected =
- "Projection: NOT test.a AS Boolean(true) OR Boolean(false) !=
test.a\
- \n TableScan: test";
-
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn now_less_than_timestamp() {
- let table_scan = test_table_scan();
-
- let ts_string = "2020-09-08T12:05:00+00:00";
- let time = chrono::Utc.timestamp_nanos(1599566400000000000i64);
-
- // cast(now() as int) < cast(to_timestamp(...) as int) + 50000_i64
- let plan =
- LogicalPlanBuilder::from(table_scan)
- .filter(
- cast_to_int64_expr(now_expr())
- .lt(cast_to_int64_expr(to_timestamp_expr(ts_string))
- + lit(50000_i64)),
- )
- .unwrap()
- .build()
- .unwrap();
-
- // Note that constant folder runs and folds the entire
- // expression down to a single constant (true)
- let expected = "Filter: Boolean(true)\
- \n TableScan: test";
- let actual = get_optimized_plan_formatted(&plan, &time);
-
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn select_date_plus_interval() {
- let table_scan = test_table_scan();
-
- let ts_string = "2020-09-08T12:05:00+00:00";
- let time = chrono::Utc.timestamp_nanos(1599566400000000000i64);
-
- // now() < cast(to_timestamp(...) as int) + 5000000000
- let schema = table_scan.schema();
-
- let date_plus_interval_expr = to_timestamp_expr(ts_string)
- .cast_to(&DataType::Date32, schema)
- .unwrap()
- + Expr::Literal(ScalarValue::IntervalDayTime(Some(123i64 << 32)));
-
- let plan = LogicalPlanBuilder::from(table_scan.clone())
- .project(vec![date_plus_interval_expr])
- .unwrap()
- .build()
- .unwrap();
-
- println!("{:?}", plan);
-
- // Note that constant folder runs and folds the entire
- // expression down to a single constant (true)
- let expected = r#"Projection: Date32("18636") AS
totimestamp(Utf8("2020-09-08T12:05:00+00:00")) + IntervalDayTime("528280977408")
- TableScan: test"#;
- let actual = get_optimized_plan_formatted(&plan, &time);
-
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn simplify_not_binary() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").gt(lit(10)).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d <= Int32(10)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_bool_and() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").gt(lit(10)).and(col("d").lt(lit(100))).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d <= Int32(10) OR test.d >= Int32(100)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_bool_or() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").gt(lit(10)).or(col("d").lt(lit(100))).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d <= Int32(10) AND test.d >= Int32(100)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_not() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").gt(lit(10)).not().not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d > Int32(10)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_null() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").is_null().not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d IS NOT NULL\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_not_null() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").is_not_null().not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d IS NULL\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_in() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").in_list(vec![lit(1), lit(2), lit(3)],
false).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d NOT IN ([Int32(1), Int32(2), Int32(3)])\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_not_in() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("d").in_list(vec![lit(1), lit(2), lit(3)], true).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d IN ([Int32(1), Int32(2), Int32(3)])\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_between() {
- let table_scan = test_table_scan();
- let qual = Expr::Between(Between::new(
- Box::new(col("d")),
- false,
- Box::new(lit(1)),
- Box::new(lit(10)),
- ));
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(qual.not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d < Int32(1) OR test.d > Int32(10)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_not_between() {
- let table_scan = test_table_scan();
- let qual = Expr::Between(Between::new(
- Box::new(col("d")),
- true,
- Box::new(lit(1)),
- Box::new(lit(10)),
- ));
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(qual.not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d >= Int32(1) AND test.d <= Int32(10)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_like() {
- let schema = Schema::new(vec![
- Field::new("a", DataType::Utf8, false),
- Field::new("b", DataType::Utf8, false),
- ]);
- let table_scan = table_scan(Some("test"), &schema, None)
- .expect("creating scan")
- .build()
- .expect("building plan");
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("a").like(col("b")).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.a NOT LIKE test.b\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_not_like() {
- let schema = Schema::new(vec![
- Field::new("a", DataType::Utf8, false),
- Field::new("b", DataType::Utf8, false),
- ]);
- let table_scan = table_scan(Some("test"), &schema, None)
- .expect("creating scan")
- .build()
- .expect("building plan");
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(col("a").not_like(col("b")).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.a LIKE test.b\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_distinct_from() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(binary_expr(col("d"), Operator::IsDistinctFrom,
lit(10)).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d IS NOT DISTINCT FROM Int32(10)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
-
- #[test]
- fn simplify_not_not_distinct_from() {
- let table_scan = test_table_scan();
-
- let plan = LogicalPlanBuilder::from(table_scan)
- .filter(binary_expr(col("d"), Operator::IsNotDistinctFrom,
lit(10)).not())
- .unwrap()
- .build()
- .unwrap();
- let expected = "Filter: test.d IS DISTINCT FROM Int32(10)\
- \n TableScan: test";
-
- assert_optimized_plan_eq(&plan, expected);
- }
}
diff --git a/datafusion/optimizer/src/lib.rs
b/datafusion/optimizer/src/simplify_expressions/mod.rs
similarity index 53%
copy from datafusion/optimizer/src/lib.rs
copy to datafusion/optimizer/src/simplify_expressions/mod.rs
index 5e8108d67..0eb3359c6 100644
--- a/datafusion/optimizer/src/lib.rs
+++ b/datafusion/optimizer/src/simplify_expressions/mod.rs
@@ -15,31 +15,11 @@
// specific language governing permissions and limitations
// under the License.
-pub mod common_subexpr_eliminate;
-pub mod decorrelate_where_exists;
-pub mod decorrelate_where_in;
-pub mod eliminate_filter;
-pub mod eliminate_limit;
+pub mod context;
pub mod expr_simplifier;
-pub mod filter_null_join_keys;
-pub mod filter_push_down;
-pub mod inline_table_scan;
-pub mod limit_push_down;
-pub mod optimizer;
-pub mod projection_push_down;
-pub mod reduce_cross_join;
-pub mod reduce_outer_join;
-pub mod scalar_subquery_to_join;
-pub mod simplify_expressions;
-pub mod single_distinct_to_groupby;
-pub mod subquery_filter_to_join;
-pub mod type_coercion;
-pub mod utils;
+pub mod simplify_exprs;
+mod utils;
-pub mod rewrite_disjunctive_predicate;
-#[cfg(test)]
-pub mod test;
-pub mod unwrap_cast_in_comparison;
-
-pub use optimizer::{OptimizerConfig, OptimizerRule};
-pub use utils::optimize_children;
+pub use context::*;
+pub use expr_simplifier::*;
+pub use simplify_exprs::*;
diff --git a/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs
b/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs
new file mode 100644
index 000000000..f5ace71b8
--- /dev/null
+++ b/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs
@@ -0,0 +1,846 @@
+// 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.
+
+//! Simplify expressions optimizer rule and implementation
+
+use super::{ExprSimplifier, SimplifyContext};
+use crate::{OptimizerConfig, OptimizerRule};
+use datafusion_common::Result;
+use datafusion_expr::{logical_plan::LogicalPlan, utils::from_plan};
+use datafusion_physical_expr::execution_props::ExecutionProps;
+
+/// Optimizer Pass that simplifies [`LogicalPlan`]s by rewriting
+/// [`Expr`]`s evaluating constants and applying algebraic
+/// simplifications
+///
+/// # Introduction
+/// It uses boolean algebra laws to simplify or reduce the number of terms in
expressions.
+///
+/// # Example:
+/// `Filter: b > 2 AND b > 2`
+/// is optimized to
+/// `Filter: b > 2`
+///
+#[derive(Default)]
+pub struct SimplifyExpressions {}
+
+impl OptimizerRule for SimplifyExpressions {
+ fn name(&self) -> &str {
+ "simplify_expressions"
+ }
+
+ fn optimize(
+ &self,
+ plan: &LogicalPlan,
+ optimizer_config: &mut OptimizerConfig,
+ ) -> Result<LogicalPlan> {
+ let mut execution_props = ExecutionProps::new();
+ execution_props.query_execution_start_time =
+ optimizer_config.query_execution_start_time();
+ self.optimize_internal(plan, &execution_props)
+ }
+}
+
+impl SimplifyExpressions {
+ fn optimize_internal(
+ &self,
+ plan: &LogicalPlan,
+ execution_props: &ExecutionProps,
+ ) -> Result<LogicalPlan> {
+ // We need to pass down the all schemas within the plan tree to
`optimize_expr` in order to
+ // to evaluate expression types. For example, a projection plan's
schema will only include
+ // projected columns. With just the projected schema, it's not
possible to infer types for
+ // expressions that references non-projected columns within the same
project plan or its
+ // children plans.
+ let info = plan
+ .all_schemas()
+ .into_iter()
+ .fold(SimplifyContext::new(execution_props), |context, schema| {
+ context.with_schema(schema.clone())
+ });
+
+ let simplifier = ExprSimplifier::new(info);
+
+ let new_inputs = plan
+ .inputs()
+ .iter()
+ .map(|input| self.optimize_internal(input, execution_props))
+ .collect::<Result<Vec<_>>>()?;
+
+ let expr = plan
+ .expressions()
+ .into_iter()
+ .map(|e| {
+ // We need to keep original expression name, if any.
+ // Constant folding should not change expression name.
+ let name = &e.display_name();
+
+ // Apply the actual simplification logic
+ let new_e = simplifier.simplify(e)?;
+
+ let new_name = &new_e.display_name();
+
+ if let (Ok(expr_name), Ok(new_expr_name)) = (name, new_name) {
+ if expr_name != new_expr_name {
+ Ok(new_e.alias(expr_name))
+ } else {
+ Ok(new_e)
+ }
+ } else {
+ Ok(new_e)
+ }
+ })
+ .collect::<Result<Vec<_>>>()?;
+
+ from_plan(plan, &expr, &new_inputs)
+ }
+}
+
+impl SimplifyExpressions {
+ #[allow(missing_docs)]
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::simplify_expressions::utils::for_test::{
+ cast_to_int64_expr, now_expr, to_timestamp_expr,
+ };
+
+ use super::*;
+ use arrow::datatypes::{DataType, Field, Schema};
+ use chrono::{DateTime, TimeZone, Utc};
+ use datafusion_common::ScalarValue;
+ use datafusion_expr::{or, Between, BinaryExpr, Cast, Operator};
+
+ use datafusion_expr::logical_plan::table_scan;
+ use datafusion_expr::{
+ and, binary_expr, col, lit, logical_plan::builder::LogicalPlanBuilder,
Expr,
+ ExprSchemable,
+ };
+
+ /// A macro to assert that one string is contained within another with
+ /// a nice error message if they are not.
+ ///
+ /// Usage: `assert_contains!(actual, expected)`
+ ///
+ /// Is a macro so test error
+ /// messages are on the same line as the failure;
+ ///
+ /// Both arguments must be convertable into Strings (Into<String>)
+ macro_rules! assert_contains {
+ ($ACTUAL: expr, $EXPECTED: expr) => {
+ let actual_value: String = $ACTUAL.into();
+ let expected_value: String = $EXPECTED.into();
+ assert!(
+ actual_value.contains(&expected_value),
+ "Can not find expected in
actual.\n\nExpected:\n{}\n\nActual:\n{}",
+ expected_value,
+ actual_value
+ );
+ };
+ }
+
+ fn test_table_scan() -> LogicalPlan {
+ let schema = Schema::new(vec![
+ Field::new("a", DataType::Boolean, false),
+ Field::new("b", DataType::Boolean, false),
+ Field::new("c", DataType::Boolean, false),
+ Field::new("d", DataType::UInt32, false),
+ ]);
+ table_scan(Some("test"), &schema, None)
+ .expect("creating scan")
+ .build()
+ .expect("building plan")
+ }
+
+ fn assert_optimized_plan_eq(plan: &LogicalPlan, expected: &str) {
+ let rule = SimplifyExpressions::new();
+ let optimized_plan = rule
+ .optimize(plan, &mut OptimizerConfig::new())
+ .expect("failed to optimize plan");
+ let formatted_plan = format!("{:?}", optimized_plan);
+ assert_eq!(formatted_plan, expected);
+ }
+
+ #[test]
+ fn test_simplify_optimized_plan() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(vec![col("a")])
+ .unwrap()
+ .filter(and(col("b").gt(lit(1)), col("b").gt(lit(1))))
+ .unwrap()
+ .build()
+ .unwrap();
+
+ assert_optimized_plan_eq(
+ &plan,
+ "\
+ Filter: test.b > Int32(1)\
+ \n Projection: test.a\
+ \n TableScan: test",
+ );
+ }
+
+ #[test]
+ fn test_simplify_optimized_plan_with_or() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(vec![col("a")])
+ .unwrap()
+ .filter(or(col("b").gt(lit(1)), col("b").gt(lit(1))))
+ .unwrap()
+ .build()
+ .unwrap();
+
+ assert_optimized_plan_eq(
+ &plan,
+ "\
+ Filter: test.b > Int32(1)\
+ \n Projection: test.a\
+ \n TableScan: test",
+ );
+ }
+
+ #[test]
+ fn test_simplify_optimized_plan_with_composed_and() {
+ let table_scan = test_table_scan();
+ // ((c > 5) AND (d < 6)) AND (c > 5) --> (c > 5) AND (d < 6)
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(vec![col("a"), col("b")])
+ .unwrap()
+ .filter(and(
+ and(col("a").gt(lit(5)), col("b").lt(lit(6))),
+ col("a").gt(lit(5)),
+ ))
+ .unwrap()
+ .build()
+ .unwrap();
+
+ assert_optimized_plan_eq(
+ &plan,
+ "\
+ Filter: test.a > Int32(5) AND test.b < Int32(6)\
+ \n Projection: test.a, test.b\
+ \n TableScan: test",
+ );
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_eq_expr() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("b").eq(lit(true)))
+ .unwrap()
+ .filter(col("c").eq(lit(false)))
+ .unwrap()
+ .project(vec![col("a")])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "\
+ Projection: test.a\
+ \n Filter: NOT test.c\
+ \n Filter: test.b\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_not_eq_expr() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("b").not_eq(lit(true)))
+ .unwrap()
+ .filter(col("c").not_eq(lit(false)))
+ .unwrap()
+ .limit(0, Some(1))
+ .unwrap()
+ .project(vec![col("a")])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "\
+ Projection: test.a\
+ \n Limit: skip=0, fetch=1\
+ \n Filter: test.c\
+ \n Filter: NOT test.b\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_and_expr() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("b").not_eq(lit(true)).and(col("c").eq(lit(true))))
+ .unwrap()
+ .project(vec![col("a")])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "\
+ Projection: test.a\
+ \n Filter: NOT test.b AND test.c\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_or_expr() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("b").not_eq(lit(true)).or(col("c").eq(lit(false))))
+ .unwrap()
+ .project(vec![col("a")])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "\
+ Projection: test.a\
+ \n Filter: NOT test.b OR NOT test.c\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_not_expr() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("b").eq(lit(false)).not())
+ .unwrap()
+ .project(vec![col("a")])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "\
+ Projection: test.a\
+ \n Filter: test.b\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_support_projection() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(vec![col("a"), col("d"), col("b").eq(lit(false))])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "\
+ Projection: test.a, test.d, NOT test.b AS test.b = Boolean(false)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_support_aggregate() {
+ let table_scan = test_table_scan();
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(vec![col("a"), col("c"), col("b")])
+ .unwrap()
+ .aggregate(
+ vec![col("a"), col("c")],
+ vec![
+ datafusion_expr::max(col("b").eq(lit(true))),
+ datafusion_expr::min(col("b")),
+ ],
+ )
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "\
+ Aggregate: groupBy=[[test.a, test.c]], aggr=[[MAX(test.b) AS
MAX(test.b = Boolean(true)), MIN(test.b)]]\
+ \n Projection: test.a, test.c, test.b\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn test_simplity_optimized_plan_support_values() {
+ let expr1 = Expr::BinaryExpr(BinaryExpr::new(
+ Box::new(lit(1)),
+ Operator::Plus,
+ Box::new(lit(2)),
+ ));
+ let expr2 = Expr::BinaryExpr(BinaryExpr::new(
+ Box::new(lit(2)),
+ Operator::Minus,
+ Box::new(lit(1)),
+ ));
+ let values = vec![vec![expr1, expr2]];
+ let plan =
LogicalPlanBuilder::values(values).unwrap().build().unwrap();
+
+ let expected = "\
+ Values: (Int32(3) AS Int32(1) + Int32(2), Int32(1) AS Int32(2) -
Int32(1))";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ // expect optimizing will result in an error, returning the error string
+ fn get_optimized_plan_err(plan: &LogicalPlan, date_time: &DateTime<Utc>)
-> String {
+ let mut config =
+ OptimizerConfig::new().with_query_execution_start_time(*date_time);
+ let rule = SimplifyExpressions::new();
+
+ let err = rule
+ .optimize(plan, &mut config)
+ .expect_err("expected optimization to fail");
+
+ err.to_string()
+ }
+
+ fn get_optimized_plan_formatted(
+ plan: &LogicalPlan,
+ date_time: &DateTime<Utc>,
+ ) -> String {
+ let mut config =
+ OptimizerConfig::new().with_query_execution_start_time(*date_time);
+ let rule = SimplifyExpressions::new();
+
+ let optimized_plan = rule
+ .optimize(plan, &mut config)
+ .expect("failed to optimize plan");
+ format!("{:?}", optimized_plan)
+ }
+
+ #[test]
+ fn to_timestamp_expr_folded() {
+ let table_scan = test_table_scan();
+ let proj = vec![to_timestamp_expr("2020-09-08T12:00:00+00:00")];
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(proj)
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "Projection: TimestampNanosecond(1599566400000000000,
None) AS totimestamp(Utf8(\"2020-09-08T12:00:00+00:00\"))\
+ \n TableScan: test"
+ .to_string();
+ let actual = get_optimized_plan_formatted(&plan, &Utc::now());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn to_timestamp_expr_wrong_arg() {
+ let table_scan = test_table_scan();
+ let proj = vec![to_timestamp_expr("I'M NOT A TIMESTAMP")];
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(proj)
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "Error parsing 'I'M NOT A TIMESTAMP' as timestamp";
+ let actual = get_optimized_plan_err(&plan, &Utc::now());
+ assert_contains!(actual, expected);
+ }
+
+ #[test]
+ fn cast_expr() {
+ let table_scan = test_table_scan();
+ let proj = vec![Expr::Cast(Cast::new(Box::new(lit("0")),
DataType::Int32))];
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(proj)
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "Projection: Int32(0) AS Utf8(\"0\")\
+ \n TableScan: test";
+ let actual = get_optimized_plan_formatted(&plan, &Utc::now());
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn cast_expr_wrong_arg() {
+ let table_scan = test_table_scan();
+ let proj = vec![Expr::Cast(Cast::new(Box::new(lit("")),
DataType::Int32))];
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(proj)
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let expected = "Cannot cast string '' to value of Int32 type";
+ let actual = get_optimized_plan_err(&plan, &Utc::now());
+ assert_contains!(actual, expected);
+ }
+
+ #[test]
+ fn multiple_now_expr() {
+ let table_scan = test_table_scan();
+ let time = Utc::now();
+ let proj = vec![
+ now_expr(),
+ Expr::Alias(Box::new(now_expr()), "t2".to_string()),
+ ];
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(proj)
+ .unwrap()
+ .build()
+ .unwrap();
+
+ // expect the same timestamp appears in both exprs
+ let actual = get_optimized_plan_formatted(&plan, &time);
+ let expected = format!(
+ "Projection: TimestampNanosecond({}, Some(\"UTC\")) AS now(),
TimestampNanosecond({}, Some(\"UTC\")) AS t2\
+ \n TableScan: test",
+ time.timestamp_nanos(),
+ time.timestamp_nanos()
+ );
+
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn simplify_and_eval() {
+ // demonstrate a case where the evaluation needs to run prior
+ // to the simplifier for it to work
+ let table_scan = test_table_scan();
+ let time = Utc::now();
+ // (true or false) != col --> !col
+ let proj = vec![lit(true).or(lit(false)).not_eq(col("a"))];
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .project(proj)
+ .unwrap()
+ .build()
+ .unwrap();
+
+ let actual = get_optimized_plan_formatted(&plan, &time);
+ let expected =
+ "Projection: NOT test.a AS Boolean(true) OR Boolean(false) !=
test.a\
+ \n TableScan: test";
+
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn now_less_than_timestamp() {
+ let table_scan = test_table_scan();
+
+ let ts_string = "2020-09-08T12:05:00+00:00";
+ let time = chrono::Utc.timestamp_nanos(1599566400000000000i64);
+
+ // cast(now() as int) < cast(to_timestamp(...) as int) + 50000_i64
+ let plan =
+ LogicalPlanBuilder::from(table_scan)
+ .filter(
+ cast_to_int64_expr(now_expr())
+ .lt(cast_to_int64_expr(to_timestamp_expr(ts_string))
+ + lit(50000_i64)),
+ )
+ .unwrap()
+ .build()
+ .unwrap();
+
+ // Note that constant folder runs and folds the entire
+ // expression down to a single constant (true)
+ let expected = "Filter: Boolean(true)\
+ \n TableScan: test";
+ let actual = get_optimized_plan_formatted(&plan, &time);
+
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn select_date_plus_interval() {
+ let table_scan = test_table_scan();
+
+ let ts_string = "2020-09-08T12:05:00+00:00";
+ let time = chrono::Utc.timestamp_nanos(1599566400000000000i64);
+
+ // now() < cast(to_timestamp(...) as int) + 5000000000
+ let schema = table_scan.schema();
+
+ let date_plus_interval_expr = to_timestamp_expr(ts_string)
+ .cast_to(&DataType::Date32, schema)
+ .unwrap()
+ + Expr::Literal(ScalarValue::IntervalDayTime(Some(123i64 << 32)));
+
+ let plan = LogicalPlanBuilder::from(table_scan.clone())
+ .project(vec![date_plus_interval_expr])
+ .unwrap()
+ .build()
+ .unwrap();
+
+ println!("{:?}", plan);
+
+ // Note that constant folder runs and folds the entire
+ // expression down to a single constant (true)
+ let expected = r#"Projection: Date32("18636") AS
totimestamp(Utf8("2020-09-08T12:05:00+00:00")) + IntervalDayTime("528280977408")
+ TableScan: test"#;
+ let actual = get_optimized_plan_formatted(&plan, &time);
+
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn simplify_not_binary() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").gt(lit(10)).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d <= Int32(10)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_bool_and() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").gt(lit(10)).and(col("d").lt(lit(100))).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d <= Int32(10) OR test.d >= Int32(100)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_bool_or() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").gt(lit(10)).or(col("d").lt(lit(100))).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d <= Int32(10) AND test.d >= Int32(100)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_not() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").gt(lit(10)).not().not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d > Int32(10)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_null() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").is_null().not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d IS NOT NULL\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_not_null() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").is_not_null().not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d IS NULL\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_in() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").in_list(vec![lit(1), lit(2), lit(3)],
false).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d NOT IN ([Int32(1), Int32(2), Int32(3)])\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_not_in() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("d").in_list(vec![lit(1), lit(2), lit(3)], true).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d IN ([Int32(1), Int32(2), Int32(3)])\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_between() {
+ let table_scan = test_table_scan();
+ let qual = Expr::Between(Between::new(
+ Box::new(col("d")),
+ false,
+ Box::new(lit(1)),
+ Box::new(lit(10)),
+ ));
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(qual.not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d < Int32(1) OR test.d > Int32(10)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_not_between() {
+ let table_scan = test_table_scan();
+ let qual = Expr::Between(Between::new(
+ Box::new(col("d")),
+ true,
+ Box::new(lit(1)),
+ Box::new(lit(10)),
+ ));
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(qual.not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d >= Int32(1) AND test.d <= Int32(10)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_like() {
+ let schema = Schema::new(vec![
+ Field::new("a", DataType::Utf8, false),
+ Field::new("b", DataType::Utf8, false),
+ ]);
+ let table_scan = table_scan(Some("test"), &schema, None)
+ .expect("creating scan")
+ .build()
+ .expect("building plan");
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("a").like(col("b")).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.a NOT LIKE test.b\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_not_like() {
+ let schema = Schema::new(vec![
+ Field::new("a", DataType::Utf8, false),
+ Field::new("b", DataType::Utf8, false),
+ ]);
+ let table_scan = table_scan(Some("test"), &schema, None)
+ .expect("creating scan")
+ .build()
+ .expect("building plan");
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(col("a").not_like(col("b")).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.a LIKE test.b\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_distinct_from() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(binary_expr(col("d"), Operator::IsDistinctFrom,
lit(10)).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d IS NOT DISTINCT FROM Int32(10)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+
+ #[test]
+ fn simplify_not_not_distinct_from() {
+ let table_scan = test_table_scan();
+
+ let plan = LogicalPlanBuilder::from(table_scan)
+ .filter(binary_expr(col("d"), Operator::IsNotDistinctFrom,
lit(10)).not())
+ .unwrap()
+ .build()
+ .unwrap();
+ let expected = "Filter: test.d IS DISTINCT FROM Int32(10)\
+ \n TableScan: test";
+
+ assert_optimized_plan_eq(&plan, expected);
+ }
+}
diff --git a/datafusion/optimizer/src/simplify_expressions/utils.rs
b/datafusion/optimizer/src/simplify_expressions/utils.rs
new file mode 100644
index 000000000..d9314e329
--- /dev/null
+++ b/datafusion/optimizer/src/simplify_expressions/utils.rs
@@ -0,0 +1,369 @@
+// 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.
+
+//! Utitity functions for expression simplification
+
+use arrow::datatypes::DECIMAL128_MAX_PRECISION;
+
+use datafusion_common::{DataFusionError, Result, ScalarValue};
+use datafusion_expr::{
+ expr::{Between, BinaryExpr},
+ expr_fn::{and, concat_ws, or},
+ lit, BuiltinScalarFunction, Expr, Operator,
+};
+
+pub static POWS_OF_TEN: [i128; 38] = [
+ 1,
+ 10,
+ 100,
+ 1000,
+ 10000,
+ 100000,
+ 1000000,
+ 10000000,
+ 100000000,
+ 1000000000,
+ 10000000000,
+ 100000000000,
+ 1000000000000,
+ 10000000000000,
+ 100000000000000,
+ 1000000000000000,
+ 10000000000000000,
+ 100000000000000000,
+ 1000000000000000000,
+ 10000000000000000000,
+ 100000000000000000000,
+ 1000000000000000000000,
+ 10000000000000000000000,
+ 100000000000000000000000,
+ 1000000000000000000000000,
+ 10000000000000000000000000,
+ 100000000000000000000000000,
+ 1000000000000000000000000000,
+ 10000000000000000000000000000,
+ 100000000000000000000000000000,
+ 1000000000000000000000000000000,
+ 10000000000000000000000000000000,
+ 100000000000000000000000000000000,
+ 1000000000000000000000000000000000,
+ 10000000000000000000000000000000000,
+ 100000000000000000000000000000000000,
+ 1000000000000000000000000000000000000,
+ 10000000000000000000000000000000000000,
+];
+
+/// returns true if `needle` is found in a chain of search_op
+/// expressions. Such as: (A AND B) AND C
+pub fn expr_contains(expr: &Expr, needle: &Expr, search_op: Operator) -> bool {
+ match expr {
+ Expr::BinaryExpr(BinaryExpr { left, op, right }) if *op == search_op
=> {
+ expr_contains(left, needle, search_op)
+ || expr_contains(right, needle, search_op)
+ }
+ _ => expr == needle,
+ }
+}
+
+pub fn is_zero(s: &Expr) -> bool {
+ match s {
+ Expr::Literal(ScalarValue::Int8(Some(0)))
+ | Expr::Literal(ScalarValue::Int16(Some(0)))
+ | Expr::Literal(ScalarValue::Int32(Some(0)))
+ | Expr::Literal(ScalarValue::Int64(Some(0)))
+ | Expr::Literal(ScalarValue::UInt8(Some(0)))
+ | Expr::Literal(ScalarValue::UInt16(Some(0)))
+ | Expr::Literal(ScalarValue::UInt32(Some(0)))
+ | Expr::Literal(ScalarValue::UInt64(Some(0))) => true,
+ Expr::Literal(ScalarValue::Float32(Some(v))) if *v == 0. => true,
+ Expr::Literal(ScalarValue::Float64(Some(v))) if *v == 0. => true,
+ Expr::Literal(ScalarValue::Decimal128(Some(v), _p, _s)) if *v == 0 =>
true,
+ _ => false,
+ }
+}
+
+pub fn is_one(s: &Expr) -> bool {
+ match s {
+ Expr::Literal(ScalarValue::Int8(Some(1)))
+ | Expr::Literal(ScalarValue::Int16(Some(1)))
+ | Expr::Literal(ScalarValue::Int32(Some(1)))
+ | Expr::Literal(ScalarValue::Int64(Some(1)))
+ | Expr::Literal(ScalarValue::UInt8(Some(1)))
+ | Expr::Literal(ScalarValue::UInt16(Some(1)))
+ | Expr::Literal(ScalarValue::UInt32(Some(1)))
+ | Expr::Literal(ScalarValue::UInt64(Some(1))) => true,
+ Expr::Literal(ScalarValue::Float32(Some(v))) if *v == 1. => true,
+ Expr::Literal(ScalarValue::Float64(Some(v))) if *v == 1. => true,
+ Expr::Literal(ScalarValue::Decimal128(Some(v), _p, _s)) => {
+ *_s < DECIMAL128_MAX_PRECISION && POWS_OF_TEN[*_s as usize] == *v
+ }
+ _ => false,
+ }
+}
+
+pub fn is_true(expr: &Expr) -> bool {
+ match expr {
+ Expr::Literal(ScalarValue::Boolean(Some(v))) => *v,
+ _ => false,
+ }
+}
+
+/// returns true if expr is a
+/// `Expr::Literal(ScalarValue::Boolean(v))` , false otherwise
+pub fn is_bool_lit(expr: &Expr) -> bool {
+ matches!(expr, Expr::Literal(ScalarValue::Boolean(_)))
+}
+
+/// Return a literal NULL value of Boolean data type
+pub fn lit_bool_null() -> Expr {
+ Expr::Literal(ScalarValue::Boolean(None))
+}
+
+pub fn is_null(expr: &Expr) -> bool {
+ match expr {
+ Expr::Literal(v) => v.is_null(),
+ _ => false,
+ }
+}
+
+pub fn is_false(expr: &Expr) -> bool {
+ match expr {
+ Expr::Literal(ScalarValue::Boolean(Some(v))) => !(*v),
+ _ => false,
+ }
+}
+
+/// returns true if `haystack` looks like (needle OP X) or (X OP needle)
+pub fn is_op_with(target_op: Operator, haystack: &Expr, needle: &Expr) -> bool
{
+ matches!(haystack, Expr::BinaryExpr(BinaryExpr { left, op, right }) if op
== &target_op && (needle == left.as_ref() || needle == right.as_ref()))
+}
+
+/// returns the contained boolean value in `expr` as
+/// `Expr::Literal(ScalarValue::Boolean(v))`.
+pub fn as_bool_lit(expr: Expr) -> Result<Option<bool>> {
+ match expr {
+ Expr::Literal(ScalarValue::Boolean(v)) => Ok(v),
+ _ => Err(DataFusionError::Internal(format!(
+ "Expected boolean literal, got {:?}",
+ expr
+ ))),
+ }
+}
+
+/// negate a Not clause
+/// input is the clause to be negated.(args of Not clause)
+/// For BinaryExpr, use the negator of op instead.
+/// not ( A > B) ===> (A <= B)
+/// For BoolExpr, not (A and B) ===> (not A) or (not B)
+/// not (A or B) ===> (not A) and (not B)
+/// not (not A) ===> A
+/// For NullExpr, not (A is not null) ===> A is null
+/// not (A is null) ===> A is not null
+/// For InList, not (A not in (..)) ===> A in (..)
+/// not (A in (..)) ===> A not in (..)
+/// For Between, not (A between B and C) ===> (A not between B and C)
+/// not (A not between B and C) ===> (A between B and C)
+/// For others, use Not clause
+pub fn negate_clause(expr: Expr) -> Expr {
+ match expr {
+ Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
+ if let Some(negated_op) = op.negate() {
+ return Expr::BinaryExpr(BinaryExpr::new(left, negated_op,
right));
+ }
+ match op {
+ // not (A and B) ===> (not A) or (not B)
+ Operator::And => {
+ let left = negate_clause(*left);
+ let right = negate_clause(*right);
+
+ or(left, right)
+ }
+ // not (A or B) ===> (not A) and (not B)
+ Operator::Or => {
+ let left = negate_clause(*left);
+ let right = negate_clause(*right);
+
+ and(left, right)
+ }
+ // use not clause
+ _ => Expr::Not(Box::new(Expr::BinaryExpr(BinaryExpr::new(
+ left, op, right,
+ )))),
+ }
+ }
+ // not (not A) ===> A
+ Expr::Not(expr) => *expr,
+ // not (A is not null) ===> A is null
+ Expr::IsNotNull(expr) => expr.is_null(),
+ // not (A is null) ===> A is not null
+ Expr::IsNull(expr) => expr.is_not_null(),
+ // not (A not in (..)) ===> A in (..)
+ // not (A in (..)) ===> A not in (..)
+ Expr::InList {
+ expr,
+ list,
+ negated,
+ } => expr.in_list(list, !negated),
+ // not (A between B and C) ===> (A not between B and C)
+ // not (A not between B and C) ===> (A between B and C)
+ Expr::Between(between) => Expr::Between(Between::new(
+ between.expr,
+ !between.negated,
+ between.low,
+ between.high,
+ )),
+ // use not clause
+ _ => Expr::Not(Box::new(expr)),
+ }
+}
+
+/// Simplify the `concat` function by
+/// 1. filtering out all `null` literals
+/// 2. concatenating contiguous literal arguments
+///
+/// For example:
+/// `concat(col(a), 'hello ', 'world', col(b), null)`
+/// will be optimized to
+/// `concat(col(a), 'hello world', col(b))`
+pub fn simpl_concat(args: Vec<Expr>) -> Result<Expr> {
+ let mut new_args = Vec::with_capacity(args.len());
+ let mut contiguous_scalar = "".to_string();
+ for arg in args {
+ match arg {
+ // filter out `null` args
+ Expr::Literal(ScalarValue::Utf8(None) |
ScalarValue::LargeUtf8(None)) => {}
+ // All literals have been converted to Utf8 or LargeUtf8 in
type_coercion.
+ // Concatenate it with the `contiguous_scalar`.
+ Expr::Literal(
+ ScalarValue::Utf8(Some(v)) | ScalarValue::LargeUtf8(Some(v)),
+ ) => contiguous_scalar += &v,
+ Expr::Literal(x) => {
+ return Err(DataFusionError::Internal(format!(
+ "The scalar {} should be casted to string type during the type
coercion.",
+ x
+ )))
+ }
+ // If the arg is not a literal, we should first push the current
`contiguous_scalar`
+ // to the `new_args` (if it is not empty) and reset it to empty
string.
+ // Then pushing this arg to the `new_args`.
+ arg => {
+ if !contiguous_scalar.is_empty() {
+ new_args.push(lit(contiguous_scalar));
+ contiguous_scalar = "".to_string();
+ }
+ new_args.push(arg);
+ }
+ }
+ }
+ if !contiguous_scalar.is_empty() {
+ new_args.push(lit(contiguous_scalar));
+ }
+
+ Ok(Expr::ScalarFunction {
+ fun: BuiltinScalarFunction::Concat,
+ args: new_args,
+ })
+}
+
+/// Simply the `concat_ws` function by
+/// 1. folding to `null` if the delimiter is null
+/// 2. filtering out `null` arguments
+/// 3. using `concat` to replace `concat_ws` if the delimiter is an empty
string
+/// 4. concatenating contiguous literals if the delimiter is a literal.
+pub fn simpl_concat_ws(delimiter: &Expr, args: &[Expr]) -> Result<Expr> {
+ match delimiter {
+ Expr::Literal(
+ ScalarValue::Utf8(delimiter) | ScalarValue::LargeUtf8(delimiter),
+ ) => {
+ match delimiter {
+ // when the delimiter is an empty string,
+ // we can use `concat` to replace `concat_ws`
+ Some(delimiter) if delimiter.is_empty() =>
simpl_concat(args.to_vec()),
+ Some(delimiter) => {
+ let mut new_args = Vec::with_capacity(args.len());
+ new_args.push(lit(delimiter));
+ let mut contiguous_scalar = None;
+ for arg in args {
+ match arg {
+ // filter out null args
+ Expr::Literal(ScalarValue::Utf8(None) |
ScalarValue::LargeUtf8(None)) => {}
+ Expr::Literal(ScalarValue::Utf8(Some(v)) |
ScalarValue::LargeUtf8(Some(v))) => {
+ match contiguous_scalar {
+ None => contiguous_scalar =
Some(v.to_string()),
+ Some(mut pre) => {
+ pre += delimiter;
+ pre += v;
+ contiguous_scalar = Some(pre)
+ }
+ }
+ }
+ Expr::Literal(s) => return
Err(DataFusionError::Internal(format!("The scalar {} should be casted to string
type during the type coercion.", s))),
+ // If the arg is not a literal, we should first
push the current `contiguous_scalar`
+ // to the `new_args` and reset it to None.
+ // Then pushing this arg to the `new_args`.
+ arg => {
+ if let Some(val) = contiguous_scalar {
+ new_args.push(lit(val));
+ }
+ new_args.push(arg.clone());
+ contiguous_scalar = None;
+ }
+ }
+ }
+ if let Some(val) = contiguous_scalar {
+ new_args.push(lit(val));
+ }
+ Ok(Expr::ScalarFunction {
+ fun: BuiltinScalarFunction::ConcatWithSeparator,
+ args: new_args,
+ })
+ }
+ // if the delimiter is null, then the value of the whole
expression is null.
+ None => Ok(Expr::Literal(ScalarValue::Utf8(None))),
+ }
+ }
+ Expr::Literal(d) => Err(DataFusionError::Internal(format!(
+ "The scalar {} should be casted to string type during the type
coercion.",
+ d
+ ))),
+ d => Ok(concat_ws(
+ d.clone(),
+ args.iter()
+ .cloned()
+ .filter(|x| !is_null(x))
+ .collect::<Vec<Expr>>(),
+ )),
+ }
+}
+
+#[cfg(test)]
+pub mod for_test {
+ use arrow::datatypes::DataType;
+ use datafusion_expr::{call_fn, lit, Cast, Expr};
+
+ pub fn now_expr() -> Expr {
+ call_fn("now", vec![]).unwrap()
+ }
+
+ pub fn cast_to_int64_expr(expr: Expr) -> Expr {
+ Expr::Cast(Cast::new(expr.into(), DataType::Int64))
+ }
+
+ pub fn to_timestamp_expr(arg: impl Into<String>) -> Expr {
+ call_fn("to_timestamp", vec![lit(arg.into())]).unwrap()
+ }
+}