This is an automated email from the ASF dual-hosted git repository. yiguolei pushed a commit to branch branch-4.2 in repository https://gitbox.apache.org/repos/asf/doris.git
commit 698a018d503214d09c683852dc189b05b9bf70cc Author: linrrarity <[email protected]> AuthorDate: Sat Oct 10 09:36:35 2026 +0800 branch-4.1: [fix](function) Fix overflow in quarter arithmetic (#68410) (#68764) pick: https://github.com/apache/doris/pull/68410 --- be/src/exprs/function/datetime_errors.h | 2 +- .../function_date_or_datetime_computation.h | 5 +- be/test/exprs/function/function_time_test.cpp | 46 +++++++++++++++++ .../functions/executable/DateTimeArithmetic.java | 6 +-- .../datetime_functions/test_quarters_add.groovy | 57 +++++++++++++++++++++- 5 files changed, 109 insertions(+), 7 deletions(-) diff --git a/be/src/exprs/function/datetime_errors.h b/be/src/exprs/function/datetime_errors.h index 853106f4ffb..90d1753a05c 100644 --- a/be/src/exprs/function/datetime_errors.h +++ b/be/src/exprs/function/datetime_errors.h @@ -55,7 +55,7 @@ template <typename DateValueType> // Throw for operations with a datelike and an integer (e.g. period) template <typename DateValueType> [[noreturn]] inline void throw_out_of_bound_date_int(const char* op, DateValueType arg0, - Int32 delta) { + Int64 delta) { throw Exception(ErrorCode::OUT_OF_BOUND, "Operation {} of {}, {} out of range", op, datelike_to_string<DateValueType>(arg0), delta); } diff --git a/be/src/exprs/function/function_date_or_datetime_computation.h b/be/src/exprs/function/function_date_or_datetime_computation.h index 279cc6b96e4..00cdae8de13 100644 --- a/be/src/exprs/function/function_date_or_datetime_computation.h +++ b/be/src/exprs/function/function_date_or_datetime_computation.h @@ -373,8 +373,9 @@ struct AddQuartersImpl { static constexpr auto name = "quarters_add"; static constexpr auto is_nullable = false; - static inline ReturnValueType execute(const InputValueType& t, Int32 delta) { - return date_time_add<TimeUnit::MONTH, PType, Int32>(t, 3 * delta); + static inline ReturnValueType execute(const InputValueType& t, Int64 delta) { + // Preserve both the month offset and the negation of INT_MIN in quarters_sub. + return date_time_add<TimeUnit::MONTH, PType, Int64>(t, 3 * delta); } static DataTypes get_variadic_argument_types() { diff --git a/be/test/exprs/function/function_time_test.cpp b/be/test/exprs/function/function_time_test.cpp index afb622c459d..e01f0402012 100644 --- a/be/test/exprs/function/function_time_test.cpp +++ b/be/test/exprs/function/function_time_test.cpp @@ -37,6 +37,52 @@ namespace doris { using namespace ut_type; +template <typename Transform> +void check_quarter_interval_overflow(const typename Transform::InputValueType& date) { + SCOPED_TRACE(Transform::name); + SCOPED_TRACE(Transform::ArgPType); + // Cover small wrapped month offsets and Int32 multiplication/negation boundaries. + for (Int32 quarters : + {1431655765, 1431655766, -1431655765, -1431655766, 715827882, 715827883, -715827882, + -715827883, std::numeric_limits<Int32>::min(), std::numeric_limits<Int32>::max()}) { + SCOPED_TRACE(quarters); + EXPECT_THROW(Transform::execute(date, quarters), Exception); + } +} + +template <typename Transform> +void check_quarter_sub_int_min(const typename Transform::InputValueType& date) { + SCOPED_TRACE(Transform::ArgPType); + try { + Transform::execute(date, std::numeric_limits<Int32>::min()); + FAIL() << "Subtracting INT_MIN quarters must report a date-range error"; + } catch (const Exception& e) { + EXPECT_EQ(e.code(), ErrorCode::OUT_OF_BOUND); + // Include the delimiter: a negative delta contains the same digits. + EXPECT_NE(e.to_string().find(", 6442450944 out of range"), std::string::npos) + << e.to_string(); + } +} + +TEST(VTimestampFunctionsTest, quarter_interval_overflow) { + DateV2Value<DateV2ValueType> date; + date.unchecked_set_time(2023, 1, 1, 0, 0, 0, 0); + DateV2Value<DateTimeV2ValueType> datetime; + datetime.unchecked_set_time(2023, 1, 1, 12, 34, 56, 123456); + TimestampTzValue timestamptz(datetime); + check_quarter_interval_overflow<AddQuartersImpl<TYPE_DATEV2>>(date); + check_quarter_interval_overflow<SubtractQuartersImpl<TYPE_DATEV2>>(date); + check_quarter_interval_overflow<AddQuartersImpl<TYPE_DATETIMEV2>>(datetime); + check_quarter_interval_overflow<SubtractQuartersImpl<TYPE_DATETIMEV2>>(datetime); + check_quarter_interval_overflow<AddQuartersImpl<TYPE_TIMESTAMPTZ>>(timestamptz); + check_quarter_interval_overflow<SubtractQuartersImpl<TYPE_TIMESTAMPTZ>>(timestamptz); + + // Both signs exceed the date range, so EXPECT_THROW alone cannot detect narrowing. + check_quarter_sub_int_min<SubtractQuartersImpl<TYPE_DATEV2>>(date); + check_quarter_sub_int_min<SubtractQuartersImpl<TYPE_DATETIMEV2>>(datetime); + check_quarter_sub_int_min<SubtractQuartersImpl<TYPE_TIMESTAMPTZ>>(timestamptz); +} + TEST(VTimestampFunctionsTest, day_of_week_test) { std::string func_name = "dayofweek"; diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeArithmetic.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeArithmetic.java index fa08569e000..458349d96c2 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeArithmetic.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeArithmetic.java @@ -746,17 +746,17 @@ public class DateTimeArithmetic { */ @ExecFunction(name = "quarters_sub") public static Expression quartersSub(DateV2Literal date, IntegerLiteral quarter) { - return quartersAdd(date, new IntegerLiteral(-quarter.getValue())); + return date.plusMonths(-3L * quarter.getValue()); } @ExecFunction(name = "quarters_sub") public static Expression quartersSub(DateTimeV2Literal date, IntegerLiteral quarter) { - return quartersAdd(date, new IntegerLiteral(-quarter.getValue())); + return date.plusMonths(-3L * quarter.getValue()); } @ExecFunction(name = "quarters_sub") public static Expression quartersSub(TimestampTzLiteral date, IntegerLiteral quarter) { - return quartersAdd(date, new IntegerLiteral(-quarter.getValue())); + return date.plusMonths(-3L * quarter.getValue()); } /** diff --git a/regression-test/suites/query_p0/sql_functions/datetime_functions/test_quarters_add.groovy b/regression-test/suites/query_p0/sql_functions/datetime_functions/test_quarters_add.groovy index 915d2eabcb4..cb1bdfce776 100644 --- a/regression-test/suites/query_p0/sql_functions/datetime_functions/test_quarters_add.groovy +++ b/regression-test/suites/query_p0/sql_functions/datetime_functions/test_quarters_add.groovy @@ -216,4 +216,59 @@ suite("test_quarters_add") { assertTrue(exception != null) } } -} \ No newline at end of file + + // Int32 multiplication wraps these to -1 and +2 months. The BE unit test covers + // the full numeric boundary set; P0 covers folding and constant/vector execution. + def intervals = [1431655765, 1431655766] + def types = ["date", "datetime(6)", "timestamptz(6)"] + for (def skipFold : [false, true]) { + sql "set debug_skip_fold_constant = ${skipFold}" + for (def type : types) { + for (def function : ["quarters_add", "quarters_sub"]) { + for (def quarters : intervals) { + test { + sql "select ${function}(cast('2023-01-01' as ${type}), cast(${quarters} as int))" + exception "out of range" + } + } + } + } + test { + sql "select quarters_add(date '9999-12-01', 1)" + exception "out of range" + } + test { + sql "select quarters_sub(date '0000-01-01', 1)" + exception "out of range" + } + } + + sql "set debug_skip_fold_constant = false" + sql "drop table if exists test_quarters_overflow" + sql """ + create table test_quarters_overflow ( + q int not null, + d date, + dt datetime(6), + tz timestamptz(6) + ) + duplicate key(q) + distributed by hash(q) buckets 1 + properties("replication_num" = "1") + """ + def rows = intervals.collect { quarters -> + "(${quarters}, '2023-01-01', '2023-01-01 12:34:56.123456', " + + "'2023-01-01 12:34:56.123456')" + } + sql "insert into test_quarters_overflow values ${rows.join(',')}" + for (def column : ["d", "dt", "tz"]) { + for (def function : ["quarters_add", "quarters_sub"]) { + for (def quarters : intervals) { + test { + sql "select ${function}(${column}, q) from test_quarters_overflow where q = ${quarters}" + exception "out of range" + } + } + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
