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

HappenLee 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 678df4e859a [improvement](function) Add dictionary fast path for day 
and week arithmetic (#67184)
678df4e859a is described below

commit 678df4e859a1a10a6a8f2fbfb726e5b676214b87
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Aug 27 19:38:54 2026 +0800

    [improvement](function) Add dictionary fast path for day and week 
arithmetic (#67184)
    
    `days_add`, `days_sub`, `weeks_add`, and `weeks_sub` on DATEV2 and
    DATETIMEV2
    currently use the generic `DateV2Value::date_add_interval` path for
    every row.
    That path constructs a `TimeInterval`, converts the date through
    second-level
    arithmetic, and rebuilds date and time fields even though day and week
    intervals
    only move the date part.
    
    This PR adds an inline `DateV2Value::date_add_days` fast path. Dates in
    the
    existing 1900-2039 day-offset dictionary use direct day-number and
    reverse-date
    lookups; inputs or results outside that dictionary retain the generic
    implementation. DATETIMEV2 keeps its time fields unchanged, and result
    range
    checks preserve the existing out-of-range behavior.
    
    An author microbenchmark measured approximately 8.8 ns to 2.6 ns per row
    for
    clustered dates and 8.8 ns to 3.5 ns for dates spread across 1950-2030.
    
    Differential coverage compares the new helper with the generic
    implementation
    across the supported date domain. Focused function tests cover leap
    years,
    boundaries, large deltas, DATEV2, DATETIMEV2, add/subtract paths, and
    out-of-range results.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [x] Unit Test
        - [ ] Regression test
        - [ ] Manual test
        - [ ] No need to test or manual test
    - Behavior changed:
        - [x] No
        - [ ] Yes
    - Does this need documentation?
        - [x] No
        - [ ] Yes
    
    Validation:
    
    - `./run-be-ut.sh --run
    
--filter='VDateTimeValueTest.date_add_days_matches_date_add_interval:VTimestampFunctionsTest.days_add_v2_test:VTimestampFunctionsTest.days_add_v2_boundary_test:VTimestampFunctionsTest.days_sub_v2_test:VTimestampFunctionsTest.weeks_add_v2_test:VTimestampFunctionsTest.weeks_add_v2_boundary_test:VTimestampFunctionsTest.weeks_sub_v2_test'
    -j16` — 7 tests passed under ASAN_UT.
    - `build-support/check-build-hygiene.sh` — passed.
    - `build-support/clang-format.sh`, `build-support/check-format.sh`, and
    `git diff --check origin/master...HEAD` — passed.
    - `build-support/run-clang-tidy.sh --base origin/master --build-dir
    be/ut_build_ASAN` — no diagnostics on changed ranges after the targeted
    suppression for GTest macro expansion; the overall command remains
    non-zero because of pre-existing diagnostics outside the diff and
    toolchain header-resolution errors such as missing `stddef.h`.
---
 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 e906c4f842a..d7929bb101c 100644
--- a/be/src/core/value/vdatetime_value.h
+++ b/be/src/core/value/vdatetime_value.h
@@ -1089,6 +1089,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);
 
@@ -1764,6 +1770,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 db1bd7441fc..cd2a369a9b3 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