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

Mryange 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 3e3bacd60e1 [fix](function) Preserve timezone offset signs in ISO 
timestamps (#68627)
3e3bacd60e1 is described below

commit 3e3bacd60e1fca0aef2ce7a3c83516d45521973b
Author: Mryange <[email protected]>
AuthorDate: Wed Sep 30 15:58:59 2026 +0800

    [fix](function) Preserve timezone offset signs in ISO timestamps (#68627)
    
    `to_iso8601(TIMESTAMPTZ)` derived the timezone sign from the hour
    component using integer division, so negative sub-hour offsets such as
    `-00:30` were emitted as `+00:30`. The formatter also could not preserve
    historical offsets containing seconds. Add a shared buffer-based
    formatter to `TimestampTzValue`, reuse it for `to_string`, TIMESTAMPTZ
    string casts, and `to_iso8601`, and preserve both the correct sign and
    offset seconds without per-row temporary strings in the vectorized ISO
    formatter.
    
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 be/src/core/value/timestamptz_value.cpp       | 10 ++++---
 be/src/core/value/timestamptz_value.h         |  3 +++
 be/src/exprs/function/cast/cast_to_string.h   |  5 ++--
 be/src/exprs/function/date_time_transforms.h  | 38 ++++-----------------------
 be/test/exprs/function/function_time_test.cpp | 29 ++++++++++++++++++++
 5 files changed, 47 insertions(+), 38 deletions(-)

diff --git a/be/src/core/value/timestamptz_value.cpp 
b/be/src/core/value/timestamptz_value.cpp
index 05114342b30..0dd310438e5 100644
--- a/be/src/core/value/timestamptz_value.cpp
+++ b/be/src/core/value/timestamptz_value.cpp
@@ -31,6 +31,12 @@ bool TimestampTzValue::from_string(const StringRef& str, 
const cctz::time_zone*
 }
 
 std::string TimestampTzValue::to_string(const cctz::time_zone& tz, int scale) 
const {
+    char buffer[64];
+    const int len = to_buffer(buffer, tz, scale);
+    return {buffer, static_cast<size_t>(len)};
+}
+
+int TimestampTzValue::to_buffer(char* buffer, const cctz::time_zone& tz, int 
scale) const {
     cctz::civil_second utc_cs(_utc_dt.year(), _utc_dt.month(), _utc_dt.day(), 
_utc_dt.hour(),
                               _utc_dt.minute(), _utc_dt.second());
 
@@ -62,8 +68,6 @@ std::string TimestampTzValue::to_string(const 
cctz::time_zone& tz, int scale) co
                               (uint8_t)civ.hour(), (uint8_t)civ.minute(), 
(uint8_t)civ.second(),
                               _utc_dt.microsecond());
 
-    char buffer[64];
-
     int len = tmp_dt.to_buffer(buffer, scale);
     // timezone +03:00
     // buffer[len++] = ' ';
@@ -81,7 +85,7 @@ std::string TimestampTzValue::to_string(const 
cctz::time_zone& tz, int scale) co
         buffer[len++] = static_cast<char>('0' + offset_seconds / 10);
         buffer[len++] = static_cast<char>('0' + offset_seconds % 10);
     }
-    return {buffer, static_cast<size_t>(len)};
+    return len;
 }
 
 bool TimestampTzValue::from_datetime(const DateV2Value<DateTimeV2ValueType>& 
origin_dt,
diff --git a/be/src/core/value/timestamptz_value.h 
b/be/src/core/value/timestamptz_value.h
index a3e3861ac2b..7409e0eb56c 100644
--- a/be/src/core/value/timestamptz_value.h
+++ b/be/src/core/value/timestamptz_value.h
@@ -63,6 +63,9 @@ public:
     // Outputs a string representation with timezone information in the format 
+03:00
     std::string to_string(const cctz::time_zone& local_time_zone, int scale = 
6) const;
 
+    // Caller is responsible for ensuring that buffer has enough space.
+    int to_buffer(char* buffer, const cctz::time_zone& local_time_zone, int 
scale = 6) const;
+
     // Parses a string, CastParameters can control whether strict mode is used
     bool from_string(const StringRef& str, const cctz::time_zone* 
local_time_zone,
                      CastParameters& params, uint32_t to_scale);
diff --git a/be/src/exprs/function/cast/cast_to_string.h 
b/be/src/exprs/function/cast/cast_to_string.h
index c8b6398177f..cadc5bdb2d5 100644
--- a/be/src/exprs/function/cast/cast_to_string.h
+++ b/be/src/exprs/function/cast/cast_to_string.h
@@ -521,8 +521,9 @@ inline void CastToString::push_timestamp_ns(const 
TimeStampNsValue& from, Buffer
 inline void CastToString::push_timestamptz(const TimestampTzValue& from, 
UInt32 scale,
                                            BufferWritable& bw,
                                            const DataTypeSerDe::FormatOptions& 
options) {
-    auto str = from.to_string(*options.timezone, scale);
-    bw.write(str.data(), str.size());
+    char buffer[64];
+    const int len = from.to_buffer(buffer, *options.timezone, scale);
+    bw.write(buffer, len);
 }
 
 // IPv4
diff --git a/be/src/exprs/function/date_time_transforms.h 
b/be/src/exprs/function/date_time_transforms.h
index 26238aa5db2..69a2e646368 100644
--- a/be/src/exprs/function/date_time_transforms.h
+++ b/be/src/exprs/function/date_time_transforms.h
@@ -265,44 +265,16 @@ struct ToIso8601Impl<TYPE_TIMESTAMPTZ> {
     static constexpr PrimitiveType OpArgType = TYPE_TIMESTAMPTZ;
     using ArgType = typename PrimitiveTypeTraits<TYPE_TIMESTAMPTZ>::CppType;
     static constexpr auto name = "to_iso8601";
-    // Format: YYYY-MM-DDTHH:MM:SS.SSSSSS+HH:MM
-    static constexpr auto max_size = 32;
+    // A historical offset can contain seconds.
+    static constexpr auto max_size = 35;
 
     static auto execute(const TimestampTzValue& tz_value, ColumnString::Chars& 
res_data,
                         size_t& offset, const char* const* /*names_ptr*/,
                         FunctionContext* context) {
-        // Get timezone
-        const auto& local_time_zone = context->state()->timezone_obj();
-
-        // Convert UTC time to local time
-        cctz::civil_second utc_sec(tz_value.year(), tz_value.month(), 
tz_value.day(),
-                                   tz_value.hour(), tz_value.minute(), 
tz_value.second());
-        cctz::time_point<cctz::seconds> local_time = cctz::convert(utc_sec, 
cctz::utc_time_zone());
-
-        auto lookup_result = local_time_zone.lookup(local_time);
-        cctz::civil_second civ = lookup_result.cs;
-        auto time_offset = lookup_result.offset;
-
-        int offset_hours = time_offset / 3600;
-        int offset_mins = (std::abs(time_offset) % 3600) / 60;
-
-        // Create local datetime value
-        DateV2Value<DateTimeV2ValueType> local_dt;
-        local_dt.unchecked_set_time((uint16_t)civ.year(), 
(uint8_t)civ.month(), (uint8_t)civ.day(),
-                                    (uint8_t)civ.hour(), (uint8_t)civ.minute(),
-                                    (uint8_t)civ.second(), 
tz_value.microsecond());
-
-        // YYYY-MM-DDTHH:MM:SS.SSSSSS+HH:MM
-        auto length = local_dt.to_buffer((char*)res_data.data() + offset, 6);
+        const int length = tz_value.to_buffer((char*)res_data.data() + offset,
+                                              
context->state()->timezone_obj());
         res_data[offset + 10] = 'T';
-        res_data[offset + length] = (offset_hours >= 0 ? '+' : '-');
-        res_data[offset + length + 1] = static_cast<char>('0' + 
std::abs(offset_hours) / 10);
-        res_data[offset + length + 2] = '0' + std::abs(offset_hours) % 10;
-        res_data[offset + length + 3] = ':';
-        res_data[offset + length + 4] = static_cast<char>('0' + offset_mins / 
10);
-        res_data[offset + length + 5] = '0' + offset_mins % 10;
-
-        offset += length + 6;
+        offset += length;
         return offset;
     }
 
diff --git a/be/test/exprs/function/function_time_test.cpp 
b/be/test/exprs/function/function_time_test.cpp
index a0e8a0cfd3e..7f0a343f71d 100644
--- a/be/test/exprs/function/function_time_test.cpp
+++ b/be/test/exprs/function/function_time_test.cpp
@@ -20,6 +20,8 @@
 #include <limits>
 #include <string>
 
+#include "cctz/time_zone.h"
+#include "core/column/column_string.h"
 #include "core/data_type/data_type_date.h"
 #include "core/data_type/data_type_date_or_datetime_v2.h"
 #include "core/data_type/data_type_date_time.h"
@@ -29,14 +31,41 @@
 #include "core/data_type/data_type_time.h"
 #include "core/types.h"
 #include "core/value/time_value.h"
+#include "core/value/timestamptz_value.h"
 #include "core/value/vdatetime_value.h"
+#include "exprs/function/date_time_transforms.h"
 #include "exprs/function/function_date_or_datetime_computation.h"
 #include "exprs/function/function_test_util.h"
+#include "testutil/mock/mock_runtime_state.h"
 #include "util/timezone_utils.h"
 
 namespace doris {
 using namespace ut_type;
 
+TEST(VTimestampFunctionsTest, iso8601_preserves_negative_subhour_offset) {
+    MockRuntimeState state;
+    state._timezone_obj = cctz::fixed_time_zone(cctz::seconds(-1800));
+    FunctionContext context;
+    context._state = &state;
+
+    DateV2Value<DateTimeV2ValueType> utc_datetime;
+    utc_datetime.unchecked_set_time(2024, 1, 1, 0, 0, 0, 0);
+    TimestampTzValue value(utc_datetime);
+    ColumnString::Chars chars;
+    chars.resize(ToIso8601Impl<TYPE_TIMESTAMPTZ>::max_size);
+    size_t offset = 0;
+    ToIso8601Impl<TYPE_TIMESTAMPTZ>::execute(value, chars, offset, nullptr, 
&context);
+
+    EXPECT_EQ(std::string(reinterpret_cast<const char*>(chars.data()), offset),
+              "2023-12-31T23:30:00.000000-00:30");
+
+    state._timezone_obj = cctz::fixed_time_zone(cctz::seconds(8 * 3600 + 5 * 
60 + 43));
+    offset = 0;
+    ToIso8601Impl<TYPE_TIMESTAMPTZ>::execute(value, chars, offset, nullptr, 
&context);
+    EXPECT_EQ(std::string(reinterpret_cast<const char*>(chars.data()), offset),
+              "2024-01-01T08:05:43.000000+08:05:43");
+}
+
 template <typename Transform>
 void check_quarter_interval_overflow(const typename Transform::InputValueType& 
date) {
     SCOPED_TRACE(Transform::name);


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

Reply via email to