From 7e7607095ba57d2dcb830a8cb0f545eeb39ac4aa Mon Sep 17 00:00:00 2001 From: TCeason Date: Wed, 26 Nov 2025 10:10:05 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(query):=20add=20DATE=20=C2=B1=20INTERV?= =?UTF-8?q?AL=20->=20DATE=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added native DateType ± IntervalType -> DateType overloads so pure date math no longer casts through timestamp and reuses the existing day/month evaluators. - Taught the binder to inspect interval literals when binding +/-: if the interval has no time component we keep the date overload, otherwise we auto-cast to the timestamp overload --- .../src/scalars/timestamp/src/interval.rs | 90 ++++++++++++++ .../it/scalars/testdata/function_list.txt | 30 +++-- .../sql/src/planner/semantic/type_check.rs | 80 +++++++++++++ .../02_0080_function_interval_addsub.test | 113 +++++++++++++----- 4 files changed, 271 insertions(+), 42 deletions(-) diff --git a/src/query/functions/src/scalars/timestamp/src/interval.rs b/src/query/functions/src/scalars/timestamp/src/interval.rs index 8626bbdbf4175..fba80bc339bbc 100644 --- a/src/query/functions/src/scalars/timestamp/src/interval.rs +++ b/src/query/functions/src/scalars/timestamp/src/interval.rs @@ -20,11 +20,13 @@ use databend_common_exception::Result; use databend_common_expression::date_helper::calc_date_to_timestamp; use databend_common_expression::date_helper::today_date; use databend_common_expression::date_helper::DateConverter; +use databend_common_expression::date_helper::EvalDaysImpl; use databend_common_expression::date_helper::EvalMonthsImpl; use databend_common_expression::error_to_null; use databend_common_expression::types::interval::interval_to_string; use databend_common_expression::types::interval::string_to_interval; use databend_common_expression::types::timestamp_tz::TimestampTzType; +use databend_common_expression::types::DateType; use databend_common_expression::types::Float64Type; use databend_common_expression::types::Int64Type; use databend_common_expression::types::IntervalType; @@ -113,6 +115,26 @@ fn register_interval_add_sub_mul(registry: &mut FunctionRegistry) { ), ); + registry.register_passthrough_nullable_2_arg::( + "plus", + |_, _, _| FunctionDomain::MayThrow, + vectorize_with_builder_2_arg::( + |date, interval, output, ctx| { + eval_date_plus(date, interval, output, ctx); + }, + ), + ); + + registry.register_passthrough_nullable_2_arg::( + "plus", + |_, _, _| FunctionDomain::MayThrow, + vectorize_with_builder_2_arg::( + |interval, date, output, ctx| { + eval_date_plus(date, interval, output, ctx); + }, + ), + ); + registry .register_passthrough_nullable_2_arg::( "plus", @@ -206,6 +228,16 @@ fn register_interval_add_sub_mul(registry: &mut FunctionRegistry) { ), ); + registry.register_passthrough_nullable_2_arg::( + "minus", + |_, _, _| FunctionDomain::MayThrow, + vectorize_with_builder_2_arg::( + |date, interval, output, ctx| { + eval_date_minus(date, interval, output, ctx); + }, + ), + ); + registry .register_passthrough_nullable_2_arg::( "minus", @@ -436,6 +468,64 @@ fn eval_timestamp_minus( } } +fn eval_date_plus( + date: i32, + interval: months_days_micros, + output: &mut Vec, + ctx: &mut EvalContext, +) { + match apply_interval_to_date(date, interval, &ctx.func_ctx.tz, true) { + Ok(result) => output.push(result), + Err(err) => { + ctx.set_error(output.len(), err); + output.push(0); + } + } +} + +fn eval_date_minus( + date: i32, + interval: months_days_micros, + output: &mut Vec, + ctx: &mut EvalContext, +) { + match apply_interval_to_date(date, interval, &ctx.func_ctx.tz, false) { + Ok(result) => output.push(result), + Err(err) => { + ctx.set_error(output.len(), err); + output.push(0); + } + } +} + +fn apply_interval_to_date( + mut date: i32, + interval: months_days_micros, + tz: &TimeZone, + is_addition: bool, +) -> std::result::Result { + if interval.microseconds() != 0 { + return Err( + "DATE +/- INTERVAL with time parts should be evaluated as TIMESTAMP".to_string(), + ); + } + + let (days, months) = if is_addition { + (interval.days(), interval.months()) + } else { + (-interval.days(), -interval.months()) + }; + + if days != 0 { + date = EvalDaysImpl::eval_date(date, days); + } + if months != 0 { + date = EvalMonthsImpl::eval_date(date, tz, months, false)?; + } + + Ok(date) +} + fn register_number_to_interval(registry: &mut FunctionRegistry) { registry.register_passthrough_nullable_1_arg::( "to_centuries", diff --git a/src/query/functions/tests/it/scalars/testdata/function_list.txt b/src/query/functions/tests/it/scalars/testdata/function_list.txt index 8f7c589f4bd31..e41fc839a997e 100644 --- a/src/query/functions/tests/it/scalars/testdata/function_list.txt +++ b/src/query/functions/tests/it/scalars/testdata/function_list.txt @@ -2918,10 +2918,12 @@ Functions overloads: 233 minus(Timestamp NULL, Int64 NULL) :: Timestamp NULL 234 minus(Interval, Interval) :: Interval 235 minus(Interval NULL, Interval NULL) :: Interval NULL -236 minus(Timestamp, Interval) :: Timestamp -237 minus(Timestamp NULL, Interval NULL) :: Timestamp NULL -238 minus(TimestampTz, Interval) :: TimestampTz -239 minus(TimestampTz NULL, Interval NULL) :: TimestampTz NULL +236 minus(Date, Interval) :: Date +237 minus(Date NULL, Interval NULL) :: Date NULL +238 minus(Timestamp, Interval) :: Timestamp +239 minus(Timestamp NULL, Interval NULL) :: Timestamp NULL +240 minus(TimestampTz, Interval) :: TimestampTz +241 minus(TimestampTz NULL, Interval NULL) :: TimestampTz NULL 0 modulo(UInt8, UInt8) :: UInt8 1 modulo(UInt8 NULL, UInt8 NULL) :: UInt8 NULL 2 modulo(UInt8, UInt16) :: UInt16 @@ -3641,14 +3643,18 @@ Functions overloads: 204 plus(Timestamp NULL, Int64 NULL) :: Timestamp NULL 205 plus(Interval, Interval) :: Interval 206 plus(Interval NULL, Interval NULL) :: Interval NULL -207 plus(Timestamp, Interval) :: Timestamp -208 plus(Timestamp NULL, Interval NULL) :: Timestamp NULL -209 plus(TimestampTz, Interval) :: TimestampTz -210 plus(TimestampTz NULL, Interval NULL) :: TimestampTz NULL -211 plus(Interval, Timestamp) :: Timestamp -212 plus(Interval NULL, Timestamp NULL) :: Timestamp NULL -213 plus(Interval, TimestampTz) :: TimestampTz -214 plus(Interval NULL, TimestampTz NULL) :: TimestampTz NULL +207 plus(Date, Interval) :: Date +208 plus(Date NULL, Interval NULL) :: Date NULL +209 plus(Interval, Date) :: Date +210 plus(Interval NULL, Date NULL) :: Date NULL +211 plus(Timestamp, Interval) :: Timestamp +212 plus(Timestamp NULL, Interval NULL) :: Timestamp NULL +213 plus(TimestampTz, Interval) :: TimestampTz +214 plus(TimestampTz NULL, Interval NULL) :: TimestampTz NULL +215 plus(Interval, Timestamp) :: Timestamp +216 plus(Interval NULL, Timestamp NULL) :: Timestamp NULL +217 plus(Interval, TimestampTz) :: TimestampTz +218 plus(Interval NULL, TimestampTz NULL) :: TimestampTz NULL 0 point_in_ellipses FACTORY 0 point_in_polygon FACTORY 1 point_in_polygon FACTORY diff --git a/src/query/sql/src/planner/semantic/type_check.rs b/src/query/sql/src/planner/semantic/type_check.rs index 8291d6f33ca66..d9b2b71e65681 100644 --- a/src/query/sql/src/planner/semantic/type_check.rs +++ b/src/query/sql/src/planner/semantic/type_check.rs @@ -3459,6 +3459,21 @@ impl<'a> TypeChecker<'a> { } Ok(Box::new((res, ty))) } + BinaryOperator::Plus | BinaryOperator::Minus => { + let name = op.to_func_name(); + let (mut left_expr, left_type) = *self.resolve(left)?; + let (mut right_expr, right_type) = *self.resolve(right)?; + self.adjust_date_interval_operands( + op, + &mut left_expr, + &left_type, + &mut right_expr, + &right_type, + )?; + self.resolve_scalar_function_call(span, name.as_str(), vec![], vec![ + left_expr, right_expr, + ]) + } other => { let name = other.to_func_name(); self.resolve_function(span, name.as_str(), vec![], &[left, right]) @@ -3466,6 +3481,71 @@ impl<'a> TypeChecker<'a> { } } + fn adjust_date_interval_operands( + &self, + op: &BinaryOperator, + left_expr: &mut ScalarExpr, + left_type: &DataType, + right_expr: &mut ScalarExpr, + right_type: &DataType, + ) -> Result<()> { + match op { + BinaryOperator::Plus => { + self.adjust_single_date_interval_operand( + left_expr, left_type, right_expr, right_type, + )?; + self.adjust_single_date_interval_operand( + right_expr, right_type, left_expr, left_type, + )?; + } + BinaryOperator::Minus => { + self.adjust_single_date_interval_operand( + left_expr, left_type, right_expr, right_type, + )?; + } + _ => {} + } + Ok(()) + } + + fn adjust_single_date_interval_operand( + &self, + date_expr: &mut ScalarExpr, + date_type: &DataType, + interval_expr: &ScalarExpr, + interval_type: &DataType, + ) -> Result<()> { + if date_type.remove_nullable() != DataType::Date + || interval_type.remove_nullable() != DataType::Interval + { + return Ok(()); + } + + if self.interval_contains_only_date_parts(interval_expr)? { + return Ok(()); + } + + // Preserve nullability when casting DATE to TIMESTAMP + let target_type = if date_type.is_nullable_or_null() { + DataType::Timestamp.wrap_nullable() + } else { + DataType::Timestamp + }; + *date_expr = wrap_cast(date_expr, &target_type); + Ok(()) + } + + fn interval_contains_only_date_parts(&self, interval_expr: &ScalarExpr) -> Result { + let expr = interval_expr.as_expr()?; + let (folded, _) = ConstantFolder::fold(&expr, &self.func_ctx, &BUILTIN_FUNCTIONS); + if let EExpr::Constant(constant) = folded { + if let Scalar::Interval(value) = constant.scalar { + return Ok(value.microseconds() == 0); + } + } + Ok(false) + } + /// Resolve unary expressions. pub fn resolve_unary_op( &mut self, diff --git a/tests/sqllogictests/suites/query/functions/02_0080_function_interval_addsub.test b/tests/sqllogictests/suites/query/functions/02_0080_function_interval_addsub.test index 1efc91f50dde8..7c8a470a41351 100644 --- a/tests/sqllogictests/suites/query/functions/02_0080_function_interval_addsub.test +++ b/tests/sqllogictests/suites/query/functions/02_0080_function_interval_addsub.test @@ -2,181 +2,181 @@ skipif mysql query T SELECT DATE '1992-03-01' + to_interval('1 year') ---- -1993-03-01 00:00:00.000000 +1993-03-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('0 month') ---- -1992-03-01 00:00:00.000000 +1992-03-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('0 month') ---- -1992-03-01 00:00:00.000000 +1992-03-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('1 month') ---- -1992-04-01 00:00:00.000000 +1992-04-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('1 month') ---- -1992-02-01 00:00:00.000000 +1992-02-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('2 month') ---- -1992-05-01 00:00:00.000000 +1992-05-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('2 month') ---- -1992-01-01 00:00:00.000000 +1992-01-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('3 month') ---- -1992-06-01 00:00:00.000000 +1992-06-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('3 month') ---- -1991-12-01 00:00:00.000000 +1991-12-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('4 month') ---- -1992-07-01 00:00:00.000000 +1992-07-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('4 month') ---- -1991-11-01 00:00:00.000000 +1991-11-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('5 month') ---- -1992-08-01 00:00:00.000000 +1992-08-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('5 month') ---- -1991-10-01 00:00:00.000000 +1991-10-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('6 month') ---- -1992-09-01 00:00:00.000000 +1992-09-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('6 month') ---- -1991-09-01 00:00:00.000000 +1991-09-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('7 month') ---- -1992-10-01 00:00:00.000000 +1992-10-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('7 month') ---- -1991-08-01 00:00:00.000000 +1991-08-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('8 month') ---- -1992-11-01 00:00:00.000000 +1992-11-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('8 month') ---- -1991-07-01 00:00:00.000000 +1991-07-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('9 month') ---- -1992-12-01 00:00:00.000000 +1992-12-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('9 month') ---- -1991-06-01 00:00:00.000000 +1991-06-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('10 month') ---- -1993-01-01 00:00:00.000000 +1993-01-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('10 month') ---- -1991-05-01 00:00:00.000000 +1991-05-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('11 month') ---- -1993-02-01 00:00:00.000000 +1993-02-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('11 month') ---- -1991-04-01 00:00:00.000000 +1991-04-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('12 month') ---- -1993-03-01 00:00:00.000000 +1993-03-01 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('12 month') ---- -1991-03-01 00:00:00.000000 +1991-03-01 skipif mysql query T SELECT DATE '1992-03-01' + to_interval('10 day') ---- -1992-03-11 00:00:00.000000 +1992-03-11 skipif mysql query T SELECT DATE '1992-03-01' - to_interval('10 day') ---- -1992-02-20 00:00:00.000000 +1992-02-20 skipif mysql query T SELECT DATE '1993-03-01' - to_interval('10 day') ---- -1993-02-19 00:00:00.000000 +1993-02-19 skipif mysql query T @@ -259,3 +259,56 @@ query T SELECT TIMESTAMP_TZ '1992-01-01 10:00:00 +0800' - to_interval('1 day') ---- 1991-12-31 10:00:00.000000 +0800 + +# Test nullable date ± interval preserves nullability +skipif mysql +statement ok +CREATE OR REPLACE TABLE nullable_date_interval_test(d DATE NULL) + +skipif mysql +statement ok +INSERT INTO nullable_date_interval_test VALUES ('2022-03-15'), (NULL), ('2022-06-20') + +skipif mysql +query T +SELECT d - INTERVAL '1 hour' FROM nullable_date_interval_test ORDER BY d NULLS LAST +---- +2022-03-14 23:00:00.000000 +2022-06-19 23:00:00.000000 +NULL + +skipif mysql +query T +SELECT d + INTERVAL '30 minutes' FROM nullable_date_interval_test ORDER BY d NULLS LAST +---- +2022-03-15 00:30:00.000000 +2022-06-20 00:30:00.000000 +NULL + +skipif mysql +query T +SELECT d - interval '2 days' FROM nullable_date_interval_test ORDER BY d NULLS LAST +---- +2022-03-13 +2022-06-18 +NULL + +skipif mysql +query T +SELECT d + interval '2 days' FROM nullable_date_interval_test ORDER BY d NULLS LAST +---- +2022-03-17 +2022-06-22 +NULL + +skipif mysql +query T +SELECT (interval '2 days') + d FROM nullable_date_interval_test ORDER BY d NULLS LAST +---- +2022-03-17 +2022-06-22 +NULL + +skipif mysql +statement ok +DROP TABLE nullable_date_interval_test From 782462a321cac568a951fcddfd29c1e96bc83ea2 Mon Sep 17 00:00:00 2001 From: TCeason Date: Thu, 27 Nov 2025 10:46:11 +0800 Subject: [PATCH 2/2] fix plus/minus --- .../sql/src/planner/semantic/type_check.rs | 35 ++++++++++++++++--- .../functions/02_0079_function_interval.test | 12 +++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/query/sql/src/planner/semantic/type_check.rs b/src/query/sql/src/planner/semantic/type_check.rs index d9b2b71e65681..a0bea33515633 100644 --- a/src/query/sql/src/planner/semantic/type_check.rs +++ b/src/query/sql/src/planner/semantic/type_check.rs @@ -3130,6 +3130,8 @@ impl<'a> TypeChecker<'a> { Self::rewrite_substring(&mut args); } + self.adjust_date_interval_function_args(func_name, &mut args)?; + // Type check let mut arguments = args.iter().map(|v| v.as_raw_expr()).collect::>(); // inject the params @@ -3508,6 +3510,29 @@ impl<'a> TypeChecker<'a> { Ok(()) } + fn adjust_date_interval_function_args( + &self, + func_name: &str, + args: &mut [ScalarExpr], + ) -> Result<()> { + if args.len() != 2 { + return Ok(()); + } + let op = if func_name.eq_ignore_ascii_case("plus") { + BinaryOperator::Plus + } else if func_name.eq_ignore_ascii_case("minus") { + BinaryOperator::Minus + } else { + return Ok(()); + }; + let (left_slice, right_slice) = args.split_at_mut(1); + let left_expr = &mut left_slice[0]; + let right_expr = &mut right_slice[0]; + let left_type = left_expr.data_type()?; + let right_type = right_expr.data_type()?; + self.adjust_date_interval_operands(&op, left_expr, &left_type, right_expr, &right_type) + } + fn adjust_single_date_interval_operand( &self, date_expr: &mut ScalarExpr, @@ -3538,10 +3563,12 @@ impl<'a> TypeChecker<'a> { fn interval_contains_only_date_parts(&self, interval_expr: &ScalarExpr) -> Result { let expr = interval_expr.as_expr()?; let (folded, _) = ConstantFolder::fold(&expr, &self.func_ctx, &BUILTIN_FUNCTIONS); - if let EExpr::Constant(constant) = folded { - if let Scalar::Interval(value) = constant.scalar { - return Ok(value.microseconds() == 0); - } + if let EExpr::Constant(Constant { + scalar: Scalar::Interval(value), + .. + }) = folded + { + return Ok(value.microseconds() == 0); } Ok(false) } diff --git a/tests/sqllogictests/suites/query/functions/02_0079_function_interval.test b/tests/sqllogictests/suites/query/functions/02_0079_function_interval.test index a347e0d325933..cedfb26572d66 100644 --- a/tests/sqllogictests/suites/query/functions/02_0079_function_interval.test +++ b/tests/sqllogictests/suites/query/functions/02_0079_function_interval.test @@ -170,6 +170,18 @@ select to_interval('120000000000 months'); ---- 00:00:00 +skipif mysql +query T +select plus('2022-02-02'::date, interval '1 day 1 second'); +---- +2022-02-03 00:00:01.000000 + +skipif mysql +query T +select plus('2022-02-02'::date, interval '1 day'); +---- +2022-02-03 + skipif mysql query T select '2022-01-01'::timestamp - '2021-01-01'::timestamp