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 64825646c56 branch-4.1: [improvement](function) Add dictionary fast 
path for day and week arithmetic #67184 (#67233)
64825646c56 is described below

commit 64825646c56859c34c7f4e6aef281cfed0bac05d
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Sep 6 23:50:02 2026 +0800

    branch-4.1: [improvement](function) Add dictionary fast path for day and 
week arithmetic #67184 (#67233)
    
    Cherry-picked from #67184
    
    Co-authored-by: Jerry Hu <[email protected]>
---
 be/src/core/value/vdatetime_value.h                | 44 +++++++++++++
 .../function_date_or_datetime_computation.h        | 26 ++++++--
 be/test/core/value/vdatetime_value_test.cpp        | 51 +++++++++++++++
 be/test/exprs/function/function_time_test.cpp      | 74 ++++++++++++++++++++++
 4 files changed, 189 insertions(+), 6 deletions(-)

diff --git a/be/src/core/value/vdatetime_value.h 
b/be/src/core/value/vdatetime_value.h
index 8d826884a24..a3a13805609 100644
--- a/be/src/core/value/vdatetime_value.h
+++ b/be/src/core/value/vdatetime_value.h
@@ -1088,6 +1088,12 @@ public:
     template <TimeUnit unit, bool need_check = true>
     bool date_add_interval(const TimeInterval& interval);
 
+    // Fast path for DAY / WEEK intervals: only the date part moves (daynr +- 
days), the time part
+    // is untouched. Defined inline below `calc_daynr` so that per-row callers 
fully inline it;
+    // falls back to the generic `date_add_interval<DAY>` outside the 
day-offset dictionary.
+    template <bool need_check = true>
+    bool date_add_days(int64_t days);
+
     template <TimeUnit unit>
     bool date_set_interval(const TimeInterval& interval);
 
@@ -1763,6 +1769,44 @@ inline uint32_t calc_daynr(uint16_t year, uint8_t month, 
uint8_t day) {
     return delsum + y / 4 - y / 100 + y / 400;
 }
 
+// DAY / WEEK fast path. Real workloads hold dates that cluster in a narrow 
range, so the two
+// dictionary lookups below (`daynr(y, m, d)` and `daynr -> date`) are 
L1-resident; measured about
+// 3x faster than the generic `date_add_interval<DAY>` (no TimeInterval, no 
second-level
+// arithmetic, fully inlined into the caller's loop). Closed-form calendar 
arithmetic was tried
+// and is ~2.5x SLOWER than the generic path: its chain of constant divisions 
costs far more than
+// two warm table loads. Everything outside the dictionary (years < 1900 or > 
2039, results
+// outside it, and year 0 with its MySQL daynr quirk) takes the generic path 
unchanged.
+template <typename T>
+template <bool need_check>
+inline bool DateV2Value<T>::date_add_days(int64_t days) {
+    if constexpr (need_check) {
+        if (!is_valid_date()) [[unlikely]] {
+            return false;
+        }
+    } else {
+        DCHECK(is_valid_date());
+    }
+    if (date_day_offset_dict::can_speed_up_calc_daynr(date_v2_value_.year_) &&
+        LIKELY(date_day_offset_dict::get_dict_init())) [[likely]] {
+        const int64_t day_nr =
+                date_day_offset_dict::get().daynr(date_v2_value_.year_, 
date_v2_value_.month_,
+                                                  date_v2_value_.day_) +
+                days;
+        // range-check before narrowing: `days` may be up to INT32 * 7
+        if (day_nr > 0 && day_nr <= DATE_MAX_DAYNR &&
+            
date_day_offset_dict::can_speed_up_daynr_to_date(static_cast<int>(day_nr))) 
[[likely]] {
+            const auto to = 
date_day_offset_dict::get()[date_day_offset_dict::get_offset_by_daynr(
+                    static_cast<int>(day_nr))];
+            date_v2_value_.year_ = to.year();
+            date_v2_value_.month_ = to.month();
+            date_v2_value_.day_ = to.day();
+            return true;
+        }
+    }
+    return date_add_interval<TimeUnit::DAY, need_check>(
+            TimeInterval(TimeUnit::DAY, days < 0 ? -days : days, days < 0));
+}
+
 class DatetimeValueUtil {
 public:
     template <bool only_time>
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 939eade4ea4..2350266efbb 100644
--- a/be/src/exprs/function/function_date_or_datetime_computation.h
+++ b/be/src/exprs/function/function_date_or_datetime_computation.h
@@ -77,13 +77,27 @@ auto date_time_add(const typename 
PrimitiveTypeTraits<ArgType>::DataType::FieldT
                    IntervalType delta) {
     // e.g.: for DatatypeDatetimeV2, cast from u64 to 
DateV2Value<DateTimeV2ValueType>
     auto ts_value = t;
-    TimeInterval interval(unit, std::abs(delta), delta < 0);
-    if (!(ts_value.template date_add_interval<unit>(interval))) [[unlikely]] {
-        throw_out_of_bound_date_int(get_time_unit_name(unit), t, delta);
-    }
+    if constexpr ((unit == TimeUnit::DAY || unit == TimeUnit::WEEK) &&
+                  is_date_v2_or_datetime_v2(ArgType)) {
+        // DAY / WEEK only move the date part. `date_add_days` is inline, so 
the whole per-row
+        // computation folds into the caller's loop: no TimeInterval, no 
second-level arithmetic,
+        // just two L1-resident dictionary lookups. The input comes from a 
column and is already
+        // a valid date, so the per-row validity pre-check is skipped 
(DCHECK'd in debug builds);
+        // the result is still range-checked.
+        const int64_t days = static_cast<int64_t>(delta) * (unit == 
TimeUnit::WEEK ? 7 : 1);
+        if (!ts_value.template date_add_days<false>(days)) [[unlikely]] {
+            throw_out_of_bound_date_int(get_time_unit_name(unit), t, delta);
+        }
+        return ts_value;
+    } else {
+        TimeInterval interval(unit, std::abs(delta), delta < 0);
+        if (!(ts_value.template date_add_interval<unit>(interval))) 
[[unlikely]] {
+            throw_out_of_bound_date_int(get_time_unit_name(unit), t, delta);
+        }
 
-    // here DateValueType = ResultDateValueType
-    return ts_value;
+        // here DateValueType = ResultDateValueType
+        return ts_value;
+    }
 }
 
 #define ADD_TIME_FUNCTION_IMPL(CLASS, NAME, UNIT)                              
                   \
diff --git a/be/test/core/value/vdatetime_value_test.cpp 
b/be/test/core/value/vdatetime_value_test.cpp
index 8cb701d97d5..6b3cd7488ae 100644
--- a/be/test/core/value/vdatetime_value_test.cpp
+++ b/be/test/core/value/vdatetime_value_test.cpp
@@ -1536,4 +1536,55 @@ TEST(VDateTimeValueTest, 
date_add_interval_edge_cases_test) {
     }
 }
 
+// `date_add_days` (the DAY/WEEK fast path) must be observationally identical 
to the generic
+// `date_add_interval<DAY>`: same success/failure, same date part, time part 
untouched.
+// GTest assertions in this exhaustive differential test expand to nested 
control flow that
+// clang-tidy counts as test-body complexity.
+// NOLINTNEXTLINE(readability-function-cognitive-complexity)
+TEST(VDateTimeValueTest, date_add_days_matches_date_add_interval) {
+    const int64_t deltas[] = {0,     1,      -1,      28,     -28,     29,     
 -29,     59,   -59,
+                              60,    -60,    61,      -61,    365,     -365,   
 366,     -366, 1000,
+                              -1000, 100000, -100000, 719528, -719528, 
3652424, -3652424};
+    int64_t compared = 0;
+    // every 97th day of the whole domain, plus the first 120 days (year-0 
quirk window)
+    for (int64_t daynr = 1; daynr <= DATE_MAX_DAYNR; daynr += (daynr < 120 ? 1 
: 97)) {
+        DateV2Value<DateV2ValueType> base_date;
+        ASSERT_TRUE(base_date.get_date_from_daynr(daynr)) << daynr;
+        DateV2Value<DateTimeV2ValueType> base_dt;
+        ASSERT_TRUE(base_dt.get_date_from_daynr(daynr)) << daynr;
+        ASSERT_TRUE(base_dt.check_range_and_set_time(0, 0, 0, 23, 59, 58, 
999999, true));
+        for (int64_t delta : deltas) {
+            {
+                auto expected = base_date;
+                auto actual = base_date;
+                const bool ok_expected = 
expected.date_add_interval<TimeUnit::DAY>(
+                        TimeInterval(TimeUnit::DAY, delta < 0 ? -delta : 
delta, delta < 0));
+                const bool ok_actual = actual.date_add_days(delta);
+                ASSERT_EQ(ok_actual, ok_expected) << base_date << " + " << 
delta;
+                if (ok_expected) {
+                    ASSERT_EQ(actual.to_int64(), expected.to_int64())
+                            << base_date << " + " << delta;
+                }
+            }
+            {
+                auto expected = base_dt;
+                auto actual = base_dt;
+                const bool ok_expected = 
expected.date_add_interval<TimeUnit::DAY>(
+                        TimeInterval(TimeUnit::DAY, delta < 0 ? -delta : 
delta, delta < 0));
+                const bool ok_actual = actual.date_add_days(delta);
+                ASSERT_EQ(ok_actual, ok_expected) << base_dt << " + " << delta;
+                if (ok_expected) {
+                    ASSERT_EQ(actual.to_int64(), expected.to_int64()) << 
base_dt << " + " << delta;
+                    ASSERT_EQ(actual.hour(), 23);
+                    ASSERT_EQ(actual.minute(), 59);
+                    ASSERT_EQ(actual.second(), 58);
+                    ASSERT_EQ(actual.microsecond(), 999999);
+                }
+            }
+            ++compared;
+        }
+    }
+    EXPECT_GT(compared, 900000);
+}
+
 } // namespace doris
diff --git a/be/test/exprs/function/function_time_test.cpp 
b/be/test/exprs/function/function_time_test.cpp
index e012c7784e9..0ddc2771a59 100644
--- a/be/test/exprs/function/function_time_test.cpp
+++ b/be/test/exprs/function/function_time_test.cpp
@@ -1049,6 +1049,80 @@ TEST(VTimestampFunctionsTest, days_add_v2_test) {
     }
 }
 
+TEST(VTimestampFunctionsTest, days_add_v2_boundary_test) {
+    std::string func_name = "days_add";
+    {
+        InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2, 
PrimitiveType::TYPE_INT};
+        DataSet data_set = {
+                // leap-day and century rules
+                {{std::string("2020-02-28"), 1}, std::string("2020-02-29")},
+                {{std::string("2021-02-28"), 1}, std::string("2021-03-01")},
+                {{std::string("1900-02-28"), 1}, std::string("1900-03-01")},
+                {{std::string("2000-02-28"), 1}, std::string("2000-02-29")},
+                {{std::string("2100-02-28"), 1}, std::string("2100-03-01")},
+                // month / year boundaries in both directions
+                {{std::string("2020-12-31"), 1}, std::string("2021-01-01")},
+                {{std::string("2021-01-01"), -1}, std::string("2020-12-31")},
+                {{std::string("2020-03-01"), -1}, std::string("2020-02-29")},
+                // large deltas
+                {{std::string("1970-01-01"), 20000}, 
std::string("2024-10-04")},
+                {{std::string("2024-10-04"), -20000}, 
std::string("1970-01-01")},
+                // domain edges
+                {{std::string("9999-12-30"), 1}, std::string("9999-12-31")},
+                {{std::string("0001-01-01"), -1}, std::string("0000-12-31")},
+                {{std::string("0000-03-01"), 1}, std::string("0000-03-02")},
+                // year-0 quirk window keeps the historical (MySQL calc_daynr) 
behaviour
+                {{std::string("0000-03-01"), -1}, std::string("0000-02-28")},
+                {{std::string("0000-01-01"), 1}, std::string("0000-01-02")},
+                {{Null(), 1}, Null()},
+        };
+        static_cast<void>(check_function<DataTypeDateV2, true>(func_name, 
input_types, data_set));
+    }
+    {
+        // the time part is untouched, including the last microsecond of the 
day
+        InputTypeSet input_types = {{PrimitiveType::TYPE_DATETIMEV2, 6}, 
PrimitiveType::TYPE_INT};
+        DataSet data_set = {
+                {{std::string("2020-02-28 23:59:59.999999"), 1},
+                 std::string("2020-02-29 23:59:59.999999")},
+                {{std::string("2021-01-01 00:00:00.000001"), -1},
+                 std::string("2020-12-31 00:00:00.000001")},
+                {{std::string("9999-12-30 23:59:59.999999"), 1},
+                 std::string("9999-12-31 23:59:59.999999")},
+        };
+        static_cast<void>(
+                check_function<DataTypeDateTimeV2, true>(func_name, 
input_types, data_set, 6));
+    }
+    {
+        // out of range must still raise
+        InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2, 
PrimitiveType::TYPE_INT};
+        DataSet data_set = {
+                {{std::string("9999-12-31"), 1}, Null()},
+        };
+        static_cast<void>(check_function<DataTypeDateV2, true>(func_name, 
input_types, data_set, -1,
+                                                               -1, true));
+    }
+    {
+        InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2, 
PrimitiveType::TYPE_INT};
+        DataSet data_set = {
+                {{std::string("0000-01-01"), -1}, Null()},
+        };
+        static_cast<void>(check_function<DataTypeDateV2, true>(func_name, 
input_types, data_set, -1,
+                                                               -1, true));
+    }
+}
+
+TEST(VTimestampFunctionsTest, weeks_add_v2_boundary_test) {
+    std::string func_name = "weeks_add";
+    InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2, 
PrimitiveType::TYPE_INT};
+    DataSet data_set = {
+            {{std::string("2020-12-31"), 1}, std::string("2021-01-07")},
+            {{std::string("2020-02-25"), 1}, std::string("2020-03-03")},
+            {{std::string("2021-01-07"), -1}, std::string("2020-12-31")},
+            {{std::string("2000-01-01"), 1043}, std::string("2019-12-28")},
+    };
+    static_cast<void>(check_function<DataTypeDateV2, true>(func_name, 
input_types, data_set));
+}
+
 TEST(VTimestampFunctionsTest, days_sub_v2_test) {
     std::string func_name = "days_sub";
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to