This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new 17e6526354 Support `interval '1 month' + date/timestamp`: Handle 
binary op interval in logical AST builder   (#6270)
17e6526354 is described below

commit 17e65263547ce9527dc5a758b97d0b84192ea982
Author: Armin Primadi <[email protected]>
AuthorDate: Tue May 9 17:02:40 2023 +0700

    Support `interval '1 month' + date/timestamp`: Handle binary op interval in 
logical AST builder   (#6270)
    
    * Handle binary op interval in logical AST builder
    
    * Fix test
    
    * Support value + value interval binary operation
    
    * Fix cargo doc
    
    * Fix parser to handle complex test cases
    
    * Add more test cases
    
    * Update datafusion/sql/src/expr/value.rs
    
    Co-authored-by: Andrew Lamb <[email protected]>
    
    * Update type_coercion.slt
    
    * Fix cargo fmt
    
    ---------
    
    Co-authored-by: Andrew Lamb <[email protected]>
---
 .../tests/sqllogictests/test_files/interval.slt    | 87 +++++++++++++++++++
 .../sqllogictests/test_files/type_coercion.slt     |  6 +-
 datafusion/sql/src/expr/mod.rs                     |  2 +
 datafusion/sql/src/expr/value.rs                   | 99 +++++++++++++++++++++-
 4 files changed, 188 insertions(+), 6 deletions(-)

diff --git a/datafusion/core/tests/sqllogictests/test_files/interval.slt 
b/datafusion/core/tests/sqllogictests/test_files/interval.slt
index 82131f5d2a..e59bdbe312 100644
--- a/datafusion/core/tests/sqllogictests/test_files/interval.slt
+++ b/datafusion/core/tests/sqllogictests/test_files/interval.slt
@@ -126,8 +126,89 @@ select interval '5' nanoseconds
 ----
 0 years 0 mons 0 days 0 hours 0 mins 0.000000005 secs
 
+# Interval with string literal addition
+query ?
+select interval '1 month' + '1 month'
+----
+0 years 2 mons 0 days 0 hours 0 mins 0.000000000 secs
+
+# Interval with string literal addition and leading field
+query ?
+select interval '1' + '1' month
+----
+0 years 2 mons 0 days 0 hours 0 mins 0.000000000 secs
+
+# Interval with nested string literal addition
+query ?
+select interval '1 month' + '1 month' + '1 month'
+----
+0 years 3 mons 0 days 0 hours 0 mins 0.000000000 secs
+
+# Interval with nested string literal addition and leading field
+query ?
+select interval '1' + '1' + '1' month
+----
+0 years 3 mons 0 days 0 hours 0 mins 0.000000000 secs
+
+# Interval mega nested string literal addition
+query ?
+select interval '1 year' + '1 month' + '1 day' + '1 hour' + '1 minute' + '1 
second' + '1 millisecond' + '1 microsecond' + '1 nanosecond'
+----
+0 years 13 mons 1 days 1 hours 1 mins 1.001001001 secs
+
+# Interval with string literal subtraction
+query ?
+select interval '1 month' - '1 day';
+----
+0 years 1 mons -1 days 0 hours 0 mins 0.000000000 secs
+
+# Interval with string literal subtraction and leading field
+query ?
+select interval '5' - '1' - '2' year;
+----
+0 years 24 mons 0 days 0 hours 0 mins 0.000000000 secs
+
+# Interval with nested string literal subtraction
+query ?
+select interval '1 month' - '1 day' - '1 hour';
+----
+0 years 1 mons -1 days -1 hours 0 mins 0.000000000 secs
 
+# Interval with nested string literal subtraction and leading field
+query ?
+select interval '10' - '1' - '1' month;
+----
+0 years 8 mons 0 days 0 hours 0 mins 0.000000000 secs
 
+# Interval mega nested string literal subtraction
+query ?
+select interval '1 year' - '1 month' - '1 day' - '1 hour' - '1 minute' - '1 
second' - '1 millisecond' - '1 microsecond' - '1 nanosecond'
+----
+0 years 11 mons -1 days -1 hours -1 mins -1.001001001 secs
+
+# Interval string literal + date
+query D
+select interval '1 month' + '1 day' + '2012-01-01'::date;
+----
+2012-02-02
+
+# Interval string literal parenthesized + date
+query D
+select ( interval '1 month' + '1 day' ) + '2012-01-01'::date;
+----
+2012-02-02
+
+# Interval nested string literal + date
+query D
+select interval '1 year' + '1 month' + '1 day' + '2012-01-01'::date
+----
+2013-02-02
+
+# Interval nested string literal subtraction + date
+query D
+select interval '1 year' - '1 month' + '1 day' + '2012-01-01'::date
+----
+2012-12-02
 
 
 
@@ -378,5 +459,11 @@ select '1 month'::interval - d from t;
 query error DataFusion error: type_coercion\ncaused by\nError during planning: 
Interval\(MonthDayNano\) \- Timestamp\(Nanosecond, None\) can't be evaluated 
because there isn't a common type to coerce the types to
 select '1 month'::interval - ts from t;
 
+# interval + date
+query D
+select interval '1 month' + '2012-01-01'::date;
+----
+2012-02-01
+
 statement ok
 drop table t
diff --git a/datafusion/core/tests/sqllogictests/test_files/type_coercion.slt 
b/datafusion/core/tests/sqllogictests/test_files/type_coercion.slt
index 41bb541eac..9aced0a3fd 100644
--- a/datafusion/core/tests/sqllogictests/test_files/type_coercion.slt
+++ b/datafusion/core/tests/sqllogictests/test_files/type_coercion.slt
@@ -42,12 +42,10 @@ SELECT '2023-05-01 12:30:00'::timestamp - interval '1 
month';
 ----
 2023-04-01T12:30:00
 
-# TODO: https://github.com/apache/arrow-datafusion/issues/6180
 # interval - date
-query error DataFusion error: This feature is not implemented: Unsupported 
interval argument\. Expected string literal, got: BinaryOp \{ left: 
Value\(SingleQuotedString\("1 month"\)\), op: Minus, right: Cast \{ expr: 
Value\(SingleQuotedString\("2023\-05\-01"\)\), data_type: Date \} \}
+query error DataFusion error: type_coercion
 select interval '1 month' - '2023-05-01'::date;
 
-# TODO: https://github.com/apache/arrow-datafusion/issues/6180
 # interval - timestamp
-query error DataFusion error: This feature is not implemented: Unsupported 
interval argument\. Expected string literal, got: BinaryOp \{ left: 
Value\(SingleQuotedString\("1 month"\)\), op: Minus, right: Cast \{ expr: 
Value\(SingleQuotedString\("2023\-05\-01 12:30:00"\)\), data_type: 
Timestamp\(None, None\) \} \}
+query error DataFusion error: type_coercion
 SELECT interval '1 month' - '2023-05-01 12:30:00'::timestamp;
diff --git a/datafusion/sql/src/expr/mod.rs b/datafusion/sql/src/expr/mod.rs
index 89f90b89e6..a281e8a985 100644
--- a/datafusion/sql/src/expr/mod.rs
+++ b/datafusion/sql/src/expr/mod.rs
@@ -140,6 +140,8 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> {
                 fractional_seconds_precision,
             } => self.sql_interval_to_expr(
                 *value,
+                schema,
+                planner_context,
                 leading_field,
                 leading_precision,
                 last_field,
diff --git a/datafusion/sql/src/expr/value.rs b/datafusion/sql/src/expr/value.rs
index f1c93d8bdc..e747bf1b2a 100644
--- a/datafusion/sql/src/expr/value.rs
+++ b/datafusion/sql/src/expr/value.rs
@@ -19,9 +19,10 @@ use crate::planner::{ContextProvider, PlannerContext, 
SqlToRel};
 use arrow::compute::kernels::cast_utils::parse_interval_month_day_nano;
 use arrow_schema::DataType;
 use datafusion_common::{DFSchema, DataFusionError, Result, ScalarValue};
-use datafusion_expr::{lit, Expr};
+use datafusion_expr::expr::BinaryExpr;
+use datafusion_expr::{lit, Expr, Operator};
 use log::debug;
-use sqlparser::ast::{DateTimeField, Expr as SQLExpr, Value};
+use sqlparser::ast::{BinaryOperator, DateTimeField, Expr as SQLExpr, Value};
 use sqlparser::parser::ParserError::ParserError;
 use std::collections::HashSet;
 
@@ -160,9 +161,17 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> {
         }
     }
 
+    /// Convert a SQL interval expression to a DataFusion logical plan
+    /// expression
+    ///
+    /// Waiting for this issue to be resolved:
+    /// `<https://github.com/sqlparser-rs/sqlparser-rs/issues/869>`
+    #[allow(clippy::too_many_arguments)]
     pub(super) fn sql_interval_to_expr(
         &self,
         value: SQLExpr,
+        schema: &DFSchema,
+        planner_context: &mut PlannerContext,
         leading_field: Option<DateTimeField>,
         leading_precision: Option<u64>,
         last_field: Option<DateTimeField>,
@@ -191,6 +200,92 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> {
             SQLExpr::Value(
                 Value::SingleQuotedString(s) | Value::DoubleQuotedString(s),
             ) => s,
+            // Support expressions like `interval '1 month' + date/timestamp`.
+            // Such expressions are parsed like this by sqlparser-rs
+            //
+            // Interval
+            //   BinaryOp
+            //     Value(StringLiteral)
+            //     Cast
+            //       Value(StringLiteral)
+            //
+            // This code rewrites them to the following:
+            //
+            // BinaryOp
+            //   Interval
+            //     Value(StringLiteral)
+            //   Cast
+            //      Value(StringLiteral)
+            SQLExpr::BinaryOp { left, op, right } => {
+                let df_op = match op {
+                    BinaryOperator::Plus => Operator::Plus,
+                    BinaryOperator::Minus => Operator::Minus,
+                    _ => {
+                        return Err(DataFusionError::NotImplemented(format!(
+                            "Unsupported interval operator: {op:?}"
+                        )));
+                    }
+                };
+                match (leading_field, left.as_ref(), right.as_ref()) {
+                    (_, _, SQLExpr::Value(_)) => {
+                        let left_expr = self.sql_interval_to_expr(
+                            *left,
+                            schema,
+                            planner_context,
+                            leading_field,
+                            None,
+                            None,
+                            None,
+                        )?;
+                        let right_expr = self.sql_interval_to_expr(
+                            *right,
+                            schema,
+                            planner_context,
+                            leading_field,
+                            None,
+                            None,
+                            None,
+                        )?;
+                        return Ok(Expr::BinaryExpr(BinaryExpr::new(
+                            Box::new(left_expr),
+                            df_op,
+                            Box::new(right_expr),
+                        )));
+                    }
+                    // In this case, the left node is part of the interval
+                    // expr and the right node is an independent expr.
+                    //
+                    // Leading field is not supported when the right operand
+                    // is not a value.
+                    (None, _, _) => {
+                        let left_expr = self.sql_interval_to_expr(
+                            *left,
+                            schema,
+                            planner_context,
+                            None,
+                            None,
+                            None,
+                            None,
+                        )?;
+                        let right_expr = self.sql_expr_to_logical_expr(
+                            *right,
+                            schema,
+                            planner_context,
+                        )?;
+                        return Ok(Expr::BinaryExpr(BinaryExpr::new(
+                            Box::new(left_expr),
+                            df_op,
+                            Box::new(right_expr),
+                        )));
+                    }
+                    _ => {
+                        let value = SQLExpr::BinaryOp { left, op, right };
+                        return Err(DataFusionError::NotImplemented(format!(
+                            "Unsupported interval argument. Expected string 
literal, got: {value:?}"
+                        )));
+                    }
+                }
+            }
             _ => {
                 return Err(DataFusionError::NotImplemented(format!(
                     "Unsupported interval argument. Expected string literal, 
got: {value:?}"

Reply via email to