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]