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

starocean999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 91d7975d566 [fix](function) Fix overflow in quarter arithmetic (#68410)
91d7975d566 is described below

commit 91d7975d566909087d783407e003c87aeaf267ab
Author: linrrarity <[email protected]>
AuthorDate: Thu Sep 24 15:46:20 2026 +0800

    [fix](function) Fix overflow in quarter arithmetic (#68410)
    
    Problem Summary:
    `quarters_add` / `quarters_sub` uses the i32 middle value when
    converting quarters to months, and the oversized INT parameter may have
    an integer backturn, bypassing the subsequent date range check and
    returning the wrong date.
    
    This repair includes:
    
    1. **BE quarter addition and subtraction**: Use Int64 to receive the
    quarterly offset and calculate the number of months to avoid overflow
    when multiplying by 3, and also avoid `quarters_sub` to narrowing back
    to i32 when calling addition after negative INT_MIN. Four date types
    share this repair, and the real bias is transferred to the existing date
    range for inspection and processing.
    
    2. **FE quarter function calculation**: complete the `quarters_add` of
    TIMESTAMP_NS, using long multiplication; four date types `quarters_sub`
    directly calculate `-3L * quarter `, avoid losing in i32 first. The
    original long addition of DATE/DATETIME/TIMESTAMPTZ and the return
    mechanism after the failure of constant folding remain unchanged.
    
    3. **Error message**: The offset parameter in the date cross-bound error
    uses Int64 to avoid truncating the number of months again when the error
    is reported.
    
    ### Release note
    
    Fix quarters_add and quarters_sub returning incorrect dates for
    excessively large quarter intervals; these inputs now report date-range
    errors.
---
 be/src/exprs/function/datetime_errors.h            |  2 +-
 .../function_date_or_datetime_computation.h        |  5 +-
 be/test/exprs/function/function_time_test.cpp      | 51 +++++++++++++++++++
 .../functions/executable/DateTimeArithmetic.java   |  2 +-
 .../datetime_functions/test_quarters_add.groovy    | 58 +++++++++++++++++++++-
 5 files changed, 113 insertions(+), 5 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 8ff45d6568f..c631504fd6b 100644
--- a/be/src/exprs/function/function_date_or_datetime_computation.h
+++ b/be/src/exprs/function/function_date_or_datetime_computation.h
@@ -422,8 +422,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 ce1b24e8932..a0e8a0cfd3e 100644
--- a/be/test/exprs/function/function_time_test.cpp
+++ b/be/test/exprs/function/function_time_test.cpp
@@ -37,6 +37,57 @@
 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);
+    TimeStampNsValue timestamp_ns;
+    ASSERT_TRUE(timestamp_ns.from_datetime(datetime, 789));
+    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);
+    
check_quarter_interval_overflow<AddQuartersImpl<TYPE_TIMESTAMP_NS>>(timestamp_ns);
+    
check_quarter_interval_overflow<SubtractQuartersImpl<TYPE_TIMESTAMP_NS>>(timestamp_ns);
+
+    // 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);
+    
check_quarter_sub_int_min<SubtractQuartersImpl<TYPE_TIMESTAMP_NS>>(timestamp_ns);
+}
+
 TEST(VTimestampFunctionsTest, current_timestamp_ns_precision_test) {
     TimezoneUtils::load_timezones_to_cache();
     InputTypeSet input_types = {ConstedNotnull {PrimitiveType::TYPE_INT}};
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 c384e41468c..8d92bcf9a1b 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
@@ -497,7 +497,7 @@ public class DateTimeArithmetic {
 
     @ExecFunction(name = "quarters_add")
     public static Expression quartersAdd(TimeStampNsLiteral date, 
IntegerLiteral quarter) {
-        return date.plusMonths(3 * quarter.getValue());
+        return date.plusMonths(Math.multiplyExact(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..8e87563693e 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,60 @@ 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)", "timestamp_ns"]
+    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),
+            ns timestamp_ns
+        )
+        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', '2023-01-01 
12:34:56.123456789')"
+    }
+    sql "insert into test_quarters_overflow values ${rows.join(',')}"
+    for (def column : ["d", "dt", "tz", "ns"]) {
+        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