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]

Reply via email to