This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 303dd0e52cc branch-4.1: [fix](function) Fix overflow in quarter
arithmetic (#68410) (#68764)
303dd0e52cc is described below
commit 303dd0e52cc3fa035c5da0886f0c0172bff2e8f6
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 | 12 ++---
.../datetime_functions/test_quarters_add.groovy | 57 +++++++++++++++++++++-
5 files changed, 112 insertions(+), 10 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 cd12c00df21..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
@@ -423,17 +423,17 @@ public class DateTimeArithmetic {
*/
@ExecFunction(name = "quarters_add")
public static Expression quartersAdd(DateV2Literal date, IntegerLiteral
quarter) {
- return date.plusMonths(3 * quarter.getValue());
+ return date.plusMonths(Math.multiplyExact(3L, quarter.getValue()));
}
@ExecFunction(name = "quarters_add")
public static Expression quartersAdd(DateTimeV2Literal date,
IntegerLiteral quarter) {
- return date.plusMonths(3 * quarter.getValue());
+ return date.plusMonths(Math.multiplyExact(3L, quarter.getValue()));
}
@ExecFunction(name = "quarters_add")
public static Expression quartersAdd(TimestampTzLiteral date,
IntegerLiteral quarter) {
- return date.plusMonths(3 * quarter.getValue());
+ return date.plusMonths(Math.multiplyExact(3L, quarter.getValue()));
}
/**
@@ -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]