Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions be/src/core/value/timestamptz_value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,36 @@ void TimestampTzValue::convert_local_to_utc(const cctz::time_zone& local_time_zo
dt.microsecond());
}

int TimestampTzValue::utc_offset(const cctz::time_zone& local_time_zone) const {
cctz::civil_second utc_cs(_utc_dt.year(), _utc_dt.month(), _utc_dt.day(), _utc_dt.hour(),
_utc_dt.minute(), _utc_dt.second());
cctz::time_point<cctz::seconds> utc_tp = cctz::convert(utc_cs, cctz::utc_time_zone());
return local_time_zone.lookup(utc_tp).offset;
}

void TimestampTzValue::convert_local_to_utc(const cctz::time_zone& local_time_zone,
const DateV2Value<DateTimeV2ValueType>& dt,
int preferred_offset) {
cctz::civil_second local_cs(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(),
dt.second());
const auto lookup = local_time_zone.lookup(local_cs);
cctz::time_point<cctz::seconds> local_tp = cctz::convert(local_cs, local_time_zone);

if (lookup.kind == cctz::time_zone::civil_lookup::REPEATED) {
const auto pre_offset = local_time_zone.lookup(lookup.pre).offset;
const auto post_offset = local_time_zone.lookup(lookup.post).offset;
if (preferred_offset == pre_offset) {
local_tp = lookup.pre;
} else if (preferred_offset == post_offset) {
local_tp = lookup.post;
}
}

auto utc_cs = cctz::convert(local_tp, cctz::utc_time_zone());
_utc_dt.unchecked_set_time((uint16_t)utc_cs.year(), (uint8_t)utc_cs.month(),
(uint8_t)utc_cs.day(), (uint8_t)utc_cs.hour(),
(uint8_t)utc_cs.minute(), (uint8_t)utc_cs.second(),
dt.microsecond());
}

} // namespace doris
7 changes: 6 additions & 1 deletion be/src/core/value/timestamptz_value.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ class TimestampTzValue {
void convert_local_to_utc(const cctz::time_zone& local_time_zone,
const DateV2Value<DateTimeV2ValueType>& dt);

int utc_offset(const cctz::time_zone& local_time_zone) const;

void convert_local_to_utc(const cctz::time_zone& local_time_zone,
const DateV2Value<DateTimeV2ValueType>& dt, int preferred_offset);

TimestampTzValue& operator++() {
++_utc_dt;
return *this;
Expand Down Expand Up @@ -197,4 +202,4 @@ struct std::hash<doris::TimestampTzValue> {
auto int_val = v.to_date_int_val();
return doris::HashUtil::hash(&int_val, sizeof(int_val), 0);
}
};
};
19 changes: 7 additions & 12 deletions be/src/exprs/function/function_datetime_floor_ceil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -771,18 +771,13 @@ struct DateTimeFloorCeilCore {
// For TimestampTzValue on date-based units, convert result from local time back to UTC
if constexpr (need_tz_conversion) {
if (result) {
cctz::civil_second local_result_cs(ts_res.year(), ts_res.month(), ts_res.day(),
ts_res.hour(), ts_res.minute(), ts_res.second());
cctz::time_point<cctz::sys_seconds> local_tp = cctz::convert(local_result_cs, tz);
auto utc_result_cs = cctz::convert(local_tp, cctz::utc_time_zone());

ts_origin.unchecked_set_time(static_cast<uint16_t>(utc_result_cs.year()),
static_cast<uint8_t>(utc_result_cs.month()),
static_cast<uint8_t>(utc_result_cs.day()),
static_cast<uint8_t>(utc_result_cs.hour()),
static_cast<uint8_t>(utc_result_cs.minute()),
static_cast<uint8_t>(utc_result_cs.second()),
ts_res.microsecond());
DateV2Value<DateTimeV2ValueType> local_result(ts_res.to_date_int_val());
if constexpr (Flag::Unit == HOUR || Flag::Unit == MINUTE) {
const int preferred_offset = ts_arg.utc_offset(tz);
ts_origin.convert_local_to_utc(tz, local_result, preferred_offset);
} else {
ts_origin.convert_local_to_utc(tz, local_result);
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion be/src/exprs/function/function_other_types_to_date.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,13 @@ struct DateTrunc {
DateV2Value<DateTimeV2ValueType> local_dt;
dt.convert_utc_to_local(timezone, local_dt);
local_dt.template datetime_trunc<Unit>();
dt.convert_local_to_utc(timezone, local_dt);
if constexpr (Unit == TimeUnit::SECOND || Unit == TimeUnit::MINUTE ||
Unit == TimeUnit::HOUR) {
const int preferred_offset = dt.utc_offset(timezone);
dt.convert_local_to_utc(timezone, local_dt, preferred_offset);
} else {
dt.convert_local_to_utc(timezone, local_dt);
}
} else {
dt.template datetime_trunc<Unit>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.doris.nereids.exceptions.CastException;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.functions.executable.DateTimeExtractAndTransform;
import org.apache.doris.nereids.trees.expressions.literal.format.DateTimeChecker;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.DateTimeType;
import org.apache.doris.nereids.types.DateTimeV2Type;
Expand Down Expand Up @@ -147,10 +148,19 @@ protected Expression uncheckedCastTo(DataType targetType) throws AnalysisExcepti
return new DateTimeLiteral((DateTimeType) targetType, datetime.year, datetime.month, datetime.day,
datetime.hour, datetime.minute, datetime.second, datetime.microSecond);
} else if (targetType.isTimeStampTzType()) {
// Explicit offsets must not round-trip through session local time; that loses the selected
// branch in DST fold hours. Wildcard targets still need a concrete scale before parsing.
TimeStampTzType timeStampTzType = (TimeStampTzType) targetType;
if (timeStampTzType.getScale() < 0) {
timeStampTzType = TimeStampTzType.forTypeFromString(value);
}
if (DateTimeChecker.hasTimeZone(value)) {
return new TimestampTzLiteral(timeStampTzType, value);
}
DateTimeV2Literal expression = castToDateTime(DateTimeV2Type.MAX, strictCast, true);
expression = (DateTimeV2Literal) (DateTimeExtractAndTransform.convertTz(expression,
new StringLiteral(ConnectContext.get().getSessionVariable().timeZone), new StringLiteral("UTC")));
return new TimestampTzLiteral((TimeStampTzType) targetType, expression.year, expression.month,
return new TimestampTzLiteral(timeStampTzType, expression.year, expression.month,
expression.day, expression.hour, expression.minute, expression.second, expression.microSecond);
} else if (targetType.isDateTimeV2Type()) {
return castToDateTime(targetType, strictCast, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,15 +638,26 @@ public static Optional<Expression> characterLiteralTypeCoercion(String value, Da
&& DateTimeChecker.isValidDateTime(value)) {
ret = DateTimeLiteral.parseDateTimeLiteral(value, true).orElse(null);
} else if (dataType.isTimeStampTzType() && DateTimeChecker.isValidDateTime(value)) {
DateTimeV2Literal dtV2Lit = (DateTimeV2Literal) DateTimeLiteral
.parseDateTimeLiteral(value, true).orElse(null);
if (dtV2Lit != null) {
dtV2Lit = (DateTimeV2Literal) (DateTimeExtractAndTransform.convertTz(
dtV2Lit,
new StringLiteral(ConnectContext.get().getSessionVariable().timeZone),
new StringLiteral("UTC")));
ret = new TimestampTzLiteral(dtV2Lit.getYear(), dtV2Lit.getMonth(), dtV2Lit.getDay(),
dtV2Lit.getHour(), dtV2Lit.getMinute(), dtV2Lit.getSecond(), dtV2Lit.getMicroSecond());
if (DateTimeChecker.hasTimeZone(value)) {
// Signature search can pass TIMESTAMPTZ(*) here. TimestampTzLiteral rounds by scale,
// so derive a concrete scale from the literal before preserving its explicit offset.
TimeStampTzType timeStampTzType = (TimeStampTzType) dataType;
if (timeStampTzType.getScale() < 0) {
timeStampTzType = TimeStampTzType.forTypeFromString(value);
}
ret = new TimestampTzLiteral(timeStampTzType, value);
} else {
DateTimeV2Literal dtV2Lit = (DateTimeV2Literal) DateTimeLiteral
.parseDateTimeLiteral(value, true).orElse(null);
if (dtV2Lit != null) {
dtV2Lit = (DateTimeV2Literal) (DateTimeExtractAndTransform.convertTz(
dtV2Lit,
new StringLiteral(ConnectContext.get().getSessionVariable().timeZone),
new StringLiteral("UTC")));
ret = new TimestampTzLiteral(dtV2Lit.getYear(), dtV2Lit.getMonth(), dtV2Lit.getDay(),
dtV2Lit.getHour(), dtV2Lit.getMinute(), dtV2Lit.getSecond(),
dtV2Lit.getMicroSecond());
}
}
} else if ((dataType.isDateV2Type() || dataType.isDateType()) && DateTimeChecker.isValidDateTime(value)) {
Result<DateLiteral, AnalysisException> parseResult = DateV2Literal.parseDateLiteral(value, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@
import org.apache.doris.nereids.types.SmallIntType;
import org.apache.doris.nereids.types.StringType;
import org.apache.doris.nereids.types.StructType;
import org.apache.doris.nereids.types.TimeStampTzType;
import org.apache.doris.nereids.types.TimeV2Type;
import org.apache.doris.nereids.types.TinyIntType;
import org.apache.doris.nereids.types.VarcharType;
import org.apache.doris.nereids.types.coercion.IntegralType;
import org.apache.doris.qe.ConnectContext;

import com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -286,6 +288,22 @@ public void testCharacterLiteralTypeCoercion() {
// datetime
Assertions.assertEquals(DateTimeV2Type.SYSTEM_DEFAULT,
TypeCoercionUtils.characterLiteralTypeCoercion("2020-02-02", DateTimeType.INSTANCE).get().getDataType());
// timestamptz wildcard
Assertions.assertEquals(TimeStampTzType.SYSTEM_DEFAULT,
TypeCoercionUtils.characterLiteralTypeCoercion("2023-08-17T01:41:18Z", TimeStampTzType.WILDCARD)
.get().getDataType());
// No-zone TIMESTAMPTZ coercion uses the session timezone to define the local civil time.
ConnectContext connectContext = new ConnectContext();
connectContext.getSessionVariable().setTimeZone("Asia/Shanghai");
connectContext.setThreadLocalInfo();
try {
// timestamptz without explicit timezone keeps the literal scale during signature search
Assertions.assertEquals(TimeStampTzType.SYSTEM_DEFAULT,
TypeCoercionUtils.characterLiteralTypeCoercion("2004-12-31", TimeStampTzType.MAX)
.get().getDataType());
} finally {
ConnectContext.remove();
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- This file is automatically generated. You should know what you did if you want to edit this
-- !sql --
1 pre_fold_utc 2024-11-03 01:05:00.000000-04:00
2 post_fold_utc 2024-11-03 01:05:00.000000-05:00
3 pre_explicit 2024-11-03 01:05:00.000000-04:00
4 post_explicit 2024-11-03 01:05:00.000000-05:00

-- !sql --
2 post_fold_utc
4 post_explicit

-- !sql --
4 2024-11-03 01:00:00.000000-05:00 2024-11-03 01:10:00.000000-05:00 2024-11-03 01:00:00.000000-05:00 2024-11-03 01:05:00.000000-05:00 2024-11-03 01:05:00.000000-05:00 2024-11-03 01:00:00.000000-05:00

-- !sql --
2024-11-03 06:05:00.000000+00:00 2024-11-03 06:05:00.000000+00:00 2024-11-03 06:00:00.000000+00:00

Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,15 @@
2025-12-12 12:12:12.123456+08:00 3023-09-09 16:09:09.000000+08:00 3023-09-09 16:09:09.000000+08:00 1023-09-09 16:09:09.000000+08:00 3023-09-09 16:09:09.000000+08:00

-- !scale_not_in --
0000-01-01 08:00:00.123456+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00
0000-01-01 08:00:00.999999+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00
2025-12-12 12:12:12.000000+08:00 3023-09-09 16:09:09.000000+08:00 2023-09-09 16:09:09.000000+08:00 2023-09-09 16:09:09.000000+08:00 2023-09-09 16:09:09.000000+08:00
2025-12-12 12:12:12.000001+08:00 3023-09-09 16:09:09.000000+08:00 3023-09-09 16:09:09.000000+08:00 1023-09-09 16:09:09.000000+08:00 3023-09-09 16:09:09.000000+08:00
2025-12-12 12:12:12.999999+08:00 3023-09-09 16:09:09.000000+08:00 3023-09-09 16:09:09.000000+08:00 1023-09-09 16:09:09.000000+08:00 3023-09-09 16:09:09.000000+08:00
9999-12-31 23:59:59.000000+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00 0000-01-01 08:00:00.000000+08:00 2023-04-05 07:59:59.000000+08:00
9999-12-31 23:59:59.000001+08:00 0000-01-01 08:00:00.000000+08:00 9999-12-31 23:59:59.999999+08:00 0000-01-01 08:00:00.000000+08:00 9999-12-31 23:59:59.999999+08:00
9999-12-31 23:59:59.123456+08:00 0000-01-01 08:00:00.000000+08:00 9999-12-31 23:59:59.999999+08:00 0000-01-01 08:00:00.000000+08:00 9999-12-31 23:59:59.999999+08:00
9999-12-31 23:59:59.999999+08:00 0000-01-01 08:00:00.000000+08:00 9999-12-31 23:59:59.999999+08:00 0000-01-01 08:00:00.000000+08:00 9999-12-31 23:59:59.999999+08:00

-- !scale_is_null --
\N 2025-12-12 12:12:12.000000+08:00 2025-12-12 12:12:12.000000+08:00 0000-01-01 08:00:00.000000+08:00 2025-12-12 12:12:12.000000+08:00
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2097,7 +2097,7 @@ da fanadur
1196389819

-- !unix_timestamp_4 --
1196386219.000000
1196386219

-- !unix_timestamp_5 --
1196389819.000000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@

-- !get_format_time_4 --
\N

-- !sql_addtime1 --
2023-10-14T22:35:22

Expand Down Expand Up @@ -154,10 +155,10 @@
2025-06-30T17:15:30.999999

-- !sql_addtime9 --
2025-10-10T17:24:36+08:00
2025-10-10 17:24:36+08:00

-- !sql_addtime10 --
2025-10-10T17:24:36.123457+08:00
2025-10-10 17:24:36.123457+08:00

-- !sql_addtime11 --
44:22:32
Expand Down Expand Up @@ -220,3 +221,4 @@

-- !sql_subtime14 --
0001-01-01 07:59:59.999999+08:00

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

suite("test_timestamptz_dst_fold") {
sql "SET enable_nereids_planner = true;"
sql "SET enable_fallback_to_original_planner = false;"

sql "DROP TABLE IF EXISTS tz_dst_fold_events;"
sql "DROP TABLE IF EXISTS tz_dst_fold_trunc_out;"
sql """
CREATE TABLE tz_dst_fold_events (
id INT,
label VARCHAR(64),
ts TIMESTAMPTZ(6)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES('replication_num' = '1');
"""

sql "SET time_zone = 'America/New_York';"
sql """
INSERT INTO tz_dst_fold_events VALUES
(1, 'pre_fold_utc', CAST('2024-11-03 05:05:00 +00:00' AS TIMESTAMPTZ(6))),
(2, 'post_fold_utc', CAST('2024-11-03 06:05:00 +00:00' AS TIMESTAMPTZ(6))),
(3, 'pre_explicit', CAST('2024-11-03 01:05:00 -04:00' AS TIMESTAMPTZ(6))),
(4, 'post_explicit', CAST('2024-11-03 01:05:00 -05:00' AS TIMESTAMPTZ(6)));
"""

sql "SET debug_skip_fold_constant = true;"
qt_sql """
SELECT id, label, CAST(ts AS VARCHAR(64)) AS rendered
FROM tz_dst_fold_events
ORDER BY id;
"""

sql "SET debug_skip_fold_constant = false;"
qt_sql """
SELECT id, label
FROM tz_dst_fold_events
WHERE ts = CAST('2024-11-03 01:05:00 -05:00' AS TIMESTAMPTZ(6))
ORDER BY id;
"""

sql "SET debug_skip_fold_constant = true;"
qt_sql """
SELECT id,
CAST(minute_floor(ts, 10) AS VARCHAR(64)) AS minute_floor_rendered,
CAST(minute_ceil(ts, 10) AS VARCHAR(64)) AS minute_ceil_rendered,
CAST(hour_floor(ts, 1) AS VARCHAR(64)) AS hour_floor_rendered,
CAST(date_trunc(ts, 'second') AS VARCHAR(64)) AS second_trunc_rendered,
CAST(date_trunc(ts, 'minute') AS VARCHAR(64)) AS minute_trunc_rendered,
CAST(date_trunc(ts, 'hour') AS VARCHAR(64)) AS hour_trunc_rendered
FROM tz_dst_fold_events
WHERE id = 4
ORDER BY id;
"""

sql """
CREATE TABLE tz_dst_fold_trunc_out (
second_trunc TIMESTAMPTZ(6),
minute_trunc TIMESTAMPTZ(6),
hour_trunc TIMESTAMPTZ(6)
)
DUPLICATE KEY(second_trunc)
DISTRIBUTED BY HASH(second_trunc) BUCKETS 1
PROPERTIES('replication_num' = '1');
"""
sql """
INSERT INTO tz_dst_fold_trunc_out
SELECT date_trunc(ts, 'second'),
date_trunc(ts, 'minute'),
date_trunc(ts, 'hour')
FROM tz_dst_fold_events
WHERE id = 4;
"""

sql "SET time_zone = '+00:00';"
qt_sql """
SELECT CAST(second_trunc AS VARCHAR(64)) AS stored_second_utc,
CAST(minute_trunc AS VARCHAR(64)) AS stored_minute_utc,
CAST(hour_trunc AS VARCHAR(64)) AS stored_hour_utc
FROM tz_dst_fold_trunc_out
ORDER BY 1, 2, 3;
"""

sql "SET time_zone = default;"
sql "SET debug_skip_fold_constant = false;"
}
Loading