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

924060929 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 092abb9dd85 [fix](fe) Align convert_tz folding with BE DST handling 
(#64029)
092abb9dd85 is described below

commit 092abb9dd851adf8de8923cd3524ac4f68c6c49f
Author: feiniaofeiafei <[email protected]>
AuthorDate: Fri Jun 5 16:51:45 2026 +0800

    [fix](fe) Align convert_tz folding with BE DST handling (#64029)
    
    Related PR: #63853
    
    Problem Summary:
    
    FE constant folding for convert_tz() used Java LocalDateTime.atZone()
    semantics. For source local times inside daylight-saving skipped
    intervals, Java preserves the minute and second while shifting to the
    post-transition offset, while BE executes convert_tz() through
    cctz::convert() and maps skipped civil times to the transition instant.
    
    This mismatch can make FE partition pruning reason about a different
    folded value from BE execution. This PR makes FE convert_tz() constant
    folding use ZoneRules transition information to match BE behavior for
    skipped and repeated local times.
---
 be/src/exprs/function/function_convert_tz.cpp      | 21 ++++++++---
 be/test/exprs/function/function_time_test.cpp      | 17 +++++++++
 .../executable/DateTimeExtractAndTransform.java    |  6 ++-
 .../trees/expressions/literal/DateTimeLiteral.java | 36 ++++++++++++++++--
 .../DateTimeExtractAndTransformTest.java           | 43 ++++++++++++++++++++++
 .../literal/TimestampTzLiteralTest.java            | 18 +++++++++
 .../timestamptz/test_timestamptz_dst_gap.out       |  4 +-
 .../datetime_functions/test_convert_tz.out         |  3 ++
 .../datetime_functions/test_convert_tz.groovy      |  2 +
 9 files changed, 136 insertions(+), 14 deletions(-)

diff --git a/be/src/exprs/function/function_convert_tz.cpp 
b/be/src/exprs/function/function_convert_tz.cpp
index 1c6932e2b66..aa2966cbc3f 100644
--- a/be/src/exprs/function/function_convert_tz.cpp
+++ b/be/src/exprs/function/function_convert_tz.cpp
@@ -212,6 +212,19 @@ private:
         }
     }
 
+    static std::pair<int64_t, int64_t> unix_timestamp_for_convert_tz(
+            const DateValueType& ts_value, const cctz::time_zone& from_tz) {
+        cctz::civil_second civil_time(ts_value.year(), ts_value.month(), 
ts_value.day(),
+                                      ts_value.hour(), ts_value.minute(), 
ts_value.second());
+        const auto lookup = from_tz.lookup(civil_time);
+        const bool skipped = lookup.kind == 
cctz::time_zone::civil_lookup::SKIPPED;
+        const auto tp = skipped ? lookup.trans : lookup.pre;
+
+        // Skipped civil times map to the transition instant. Do not keep the
+        // input fractional part inside a local time interval that never 
existed.
+        return {tp.time_since_epoch().count(), skipped ? 0 : 
ts_value.microsecond()};
+    }
+
     static void execute_tz_const_with_state(ConvertTzState* convert_tz_state,
                                             const ColumnType* date_column,
                                             ColumnType* result_column, 
NullMap& result_null_map,
@@ -239,9 +252,7 @@ private:
             DateValueType ts_value = date_column->get_element(i);
             DateValueType ts_value2;
 
-            std::pair<int64_t, int64_t> timestamp;
-            ts_value.unix_timestamp(&timestamp, from_tz);
-            ts_value2.from_unixtime(timestamp, to_tz);
+            ts_value2.from_unixtime(unix_timestamp_for_convert_tz(ts_value, 
from_tz), to_tz);
 
             if (!ts_value2.is_valid_date()) [[unlikely]] {
                 
throw_out_of_bound_convert_tz<DateValueType>(date_column->get_element(i),
@@ -292,9 +303,7 @@ private:
                             to_tz_name);
         }
 
-        std::pair<int64_t, int64_t> timestamp;
-        ts_value.unix_timestamp(&timestamp, from_tz);
-        ts_value2.from_unixtime(timestamp, to_tz);
+        ts_value2.from_unixtime(unix_timestamp_for_convert_tz(ts_value, 
from_tz), to_tz);
 
         if (!ts_value2.is_valid_date()) [[unlikely]] {
             
throw_out_of_bound_convert_tz<DateValueType>(date_column->get_element(index_now),
diff --git a/be/test/exprs/function/function_time_test.cpp 
b/be/test/exprs/function/function_time_test.cpp
index 3043e65d592..e012c7784e9 100644
--- a/be/test/exprs/function/function_time_test.cpp
+++ b/be/test/exprs/function/function_time_test.cpp
@@ -326,6 +326,23 @@ TEST(VTimestampFunctionsTest, convert_tz_test) {
                 check_function<DataTypeDateTimeV2, true>(func_name, 
input_types, data_set));
     }
 
+    {
+        InputTypeSet input_types_scale6 = {{PrimitiveType::TYPE_DATETIMEV2, 6},
+                                           PrimitiveType::TYPE_VARCHAR,
+                                           PrimitiveType::TYPE_VARCHAR};
+        DataSet data_set = {{{std::string {"2021-03-28 02:15:30.123456"},
+                              std::string {"Europe/Paris"}, std::string 
{"UTC"}},
+                             std::string("2021-03-28 01:00:00.000000")},
+                            {{std::string {"2021-03-28 03:00:30.123456"},
+                              std::string {"Europe/Paris"}, std::string 
{"UTC"}},
+                             std::string("2021-03-28 01:00:30.123456")},
+                            {{std::string {"2021-10-31 02:15:30.123456"},
+                              std::string {"Europe/Paris"}, std::string 
{"UTC"}},
+                             std::string("2021-10-31 00:15:30.123456")}};
+        static_cast<void>(check_function<DataTypeDateTimeV2, true>(func_name, 
input_types_scale6,
+                                                                   data_set, 
6));
+    }
+
     {
         DataSet data_set = {{{std::string {"2019-08-01 02:18:27"}, std::string 
{"Asia/Shanghai"},
                               std::string {"UTC"}},
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
index acbdfc25504..f2324a396e7 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransform.java
@@ -26,6 +26,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.scalar.FromMilliseco
 import org.apache.doris.nereids.trees.expressions.functions.scalar.FromSecond;
 import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DateTimeLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
 import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
 import org.apache.doris.nereids.trees.expressions.literal.DecimalLiteral;
@@ -814,8 +815,9 @@ public class DateTimeExtractAndTransform {
         ZoneId toZone = 
ZoneId.from(zoneFormatter.parse(toTz.getStringValue()));
 
         LocalDateTime localDateTime = datetime.toJavaDateType();
-        ZonedDateTime resultDateTime = 
localDateTime.atZone(fromZone).withZoneSameInstant(toZone);
-        return 
DateTimeV2Literal.fromJavaDateType(resultDateTime.toLocalDateTime(), 
datetime.getDataType().getScale());
+        Instant instant = DateTimeLiteral.convertLocalToInstant(localDateTime, 
fromZone);
+        return 
DateTimeV2Literal.fromJavaDateType(LocalDateTime.ofInstant(instant, toZone),
+                datetime.getDataType().getScale());
     }
 
     private static void validateTimezoneOffset(String timezone) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/DateTimeLiteral.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/DateTimeLiteral.java
index d6efcb57e3c..019e8d8a40f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/DateTimeLiteral.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/DateTimeLiteral.java
@@ -37,11 +37,16 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.math.BigInteger;
+import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.time.ZoneOffset;
 import java.time.temporal.ChronoField;
 import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalQueries;
+import java.time.zone.ZoneOffsetTransition;
+import java.time.zone.ZoneRules;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -271,10 +276,33 @@ public class DateTimeLiteral extends DateLiteral {
 
     private static LocalDateTime convertTimeZone(long year, long month, long 
day, long hour, long minute,
             long second, ZoneId fromZone, ZoneId toZone) {
-        return LocalDateTime.of((int) year, (int) month, (int) day, (int) 
hour, (int) minute, (int) second)
-                .atZone(fromZone)
-                .withZoneSameInstant(toZone)
-                .toLocalDateTime();
+        LocalDateTime localDateTime = LocalDateTime.of((int) year, (int) 
month, (int) day,
+                (int) hour, (int) minute, (int) second);
+        Instant instant = convertLocalToInstant(localDateTime, fromZone);
+        return LocalDateTime.ofInstant(instant, toZone);
+    }
+
+    /**
+     * Convert a local civil datetime in {@code fromZone} to an instant with 
the same DST transition
+     * policy as BE cctz::convert(civil_second, zone).
+     *
+     * <p>For normal local times, there is one valid offset. For fall-back 
overlap times, two offsets
+     * are valid and the first one is the pre-transition offset. For 
spring-forward gap times, the
+     * local time does not exist, so any value inside the skipped interval 
maps to the transition
+     * instant.
+     */
+    public static Instant convertLocalToInstant(LocalDateTime localDateTime, 
ZoneId fromZone) {
+        ZoneRules rules = fromZone.getRules();
+        List<ZoneOffset> validOffsets = rules.getValidOffsets(localDateTime);
+        int size = validOffsets.size();
+        // Match BE cctz::convert(civil_second, zone) semantics for constant 
folding.
+        // Normal local time has one offset; repeated local time uses the 
pre-transition offset.
+        if (size == 1 || size == 2) {
+            return localDateTime.atOffset(validOffsets.get(0)).toInstant();
+        }
+        // Skipped local time maps to the transition instant, e.g. 2021-03-28 
02:15 Europe/Paris.
+        ZoneOffsetTransition transition = rules.getTransition(localDateTime);
+        return transition.getInstant();
     }
 
     public boolean checkRange() {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
index ee3604f83de..1423620f4eb 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/executable/DateTimeExtractAndTransformTest.java
@@ -24,6 +24,7 @@ import 
org.apache.doris.nereids.trees.expressions.literal.DecimalV3Literal;
 import org.apache.doris.nereids.trees.expressions.literal.SmallIntLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
 import org.apache.doris.nereids.types.DateTimeV2Type;
 
 import org.junit.jupiter.api.Assertions;
@@ -166,4 +167,46 @@ class DateTimeExtractAndTransformTest {
         Assertions.assertThrows(AnalysisException.class,
                 () -> DateTimeExtractAndTransform.fromUnixTime(dec));
     }
+
+    @Test
+    void testConvertTzDstTransition() {
+        // Spring gap maps skipped local times to the transition instant.
+        Assertions.assertEquals(
+                new DateTimeV2Literal("2021-03-28 01:00:00"),
+                DateTimeExtractAndTransform.convertTz(
+                        new DateTimeV2Literal("2021-03-28 02:15:00"),
+                        new VarcharLiteral("Europe/Paris"),
+                        new VarcharLiteral("UTC")));
+        Assertions.assertEquals(
+                new DateTimeV2Literal("2021-03-28 01:00:00"),
+                DateTimeExtractAndTransform.convertTz(
+                        new DateTimeV2Literal("2021-03-28 02:00:00"),
+                        new VarcharLiteral("Europe/Paris"),
+                        new VarcharLiteral("UTC")));
+        Assertions.assertEquals(
+                new DateTimeV2Literal("2021-03-28 01:00:00"),
+                DateTimeExtractAndTransform.convertTz(
+                        new DateTimeV2Literal("2021-03-28 03:00:00"),
+                        new VarcharLiteral("Europe/Paris"),
+                        new VarcharLiteral("UTC")));
+        // Fall overlap uses the pre-transition offset.
+        Assertions.assertEquals(
+                new DateTimeV2Literal("2021-10-31 00:15:00"),
+                DateTimeExtractAndTransform.convertTz(
+                        new DateTimeV2Literal("2021-10-31 02:15:00"),
+                        new VarcharLiteral("Europe/Paris"),
+                        new VarcharLiteral("UTC")));
+        Assertions.assertEquals(
+                new DateTimeV2Literal("2021-10-31 00:00:00"),
+                DateTimeExtractAndTransform.convertTz(
+                        new DateTimeV2Literal("2021-10-31 02:00:00"),
+                        new VarcharLiteral("Europe/Paris"),
+                        new VarcharLiteral("UTC")));
+        Assertions.assertEquals(
+                new DateTimeV2Literal("2021-10-31 02:00:00"),
+                DateTimeExtractAndTransform.convertTz(
+                        new DateTimeV2Literal("2021-10-31 03:00:00"),
+                        new VarcharLiteral("Europe/Paris"),
+                        new VarcharLiteral("UTC")));
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java
index a1961a61fbb..3e000de8a82 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java
@@ -75,6 +75,24 @@ class TimestampTzLiteralTest {
         Assertions.assertEquals(0, literal.minute);
         Assertions.assertEquals(0, literal.second);
         Assertions.assertEquals(0, literal.microSecond);
+
+        literal = new TimestampTzLiteral(TimeStampTzType.of(6), "2024-03-10 
02:30:00 America/New_York");
+        Assertions.assertEquals(2024, literal.year);
+        Assertions.assertEquals(3, literal.month);
+        Assertions.assertEquals(10, literal.day);
+        Assertions.assertEquals(7, literal.hour);
+        Assertions.assertEquals(0, literal.minute);
+        Assertions.assertEquals(0, literal.second);
+        Assertions.assertEquals(0, literal.microSecond);
+
+        literal = new TimestampTzLiteral(TimeStampTzType.of(6), "2024-11-03 
01:30:00 America/New_York");
+        Assertions.assertEquals(2024, literal.year);
+        Assertions.assertEquals(11, literal.month);
+        Assertions.assertEquals(3, literal.day);
+        Assertions.assertEquals(5, literal.hour);
+        Assertions.assertEquals(30, literal.minute);
+        Assertions.assertEquals(0, literal.second);
+        Assertions.assertEquals(0, literal.microSecond);
     }
 
     @Test
diff --git 
a/regression-test/data/datatype_p0/timestamptz/test_timestamptz_dst_gap.out 
b/regression-test/data/datatype_p0/timestamptz/test_timestamptz_dst_gap.out
index ae3b0c924d0..01c3cdfbdf2 100644
--- a/regression-test/data/datatype_p0/timestamptz/test_timestamptz_dst_gap.out
+++ b/regression-test/data/datatype_p0/timestamptz/test_timestamptz_dst_gap.out
@@ -1,9 +1,9 @@
 -- This file is automatically generated. You should know what you did if you 
want to edit this
 -- !sql --
-1      named_gap_ny    2024-03-10 07:30:00.000000+00:00
+1      named_gap_ny    2024-03-10 07:00:00.000000+00:00
 2      explicit_before_gap     2024-03-10 06:30:00.000000+00:00
 3      explicit_after_gap      2024-03-10 07:30:00.000000+00:00
-4      implicit_gap_ny 2024-03-10 07:30:00.000000+00:00
+4      implicit_gap_ny 2024-03-10 07:00:00.000000+00:00
 5      implicit_after_gap      2024-03-10 07:30:00.000000+00:00
 
 -- !sql --
diff --git 
a/regression-test/data/query_p0/sql_functions/datetime_functions/test_convert_tz.out
 
b/regression-test/data/query_p0/sql_functions/datetime_functions/test_convert_tz.out
index 9bf49bd74ef..643540d9c7b 100644
--- 
a/regression-test/data/query_p0/sql_functions/datetime_functions/test_convert_tz.out
+++ 
b/regression-test/data/query_p0/sql_functions/datetime_functions/test_convert_tz.out
@@ -21,3 +21,6 @@
 2024-04-18T23:20
 2024-04-18T23:20
 
+-- !spring_gp_with_micro_sec --
+2021-03-28T01:00
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/datetime_functions/test_convert_tz.groovy
 
b/regression-test/suites/query_p0/sql_functions/datetime_functions/test_convert_tz.groovy
index e382fe9ae70..c9f37b37b4f 100644
--- 
a/regression-test/suites/query_p0/sql_functions/datetime_functions/test_convert_tz.groovy
+++ 
b/regression-test/suites/query_p0/sql_functions/datetime_functions/test_convert_tz.groovy
@@ -35,4 +35,6 @@ suite("test_convert_tz") {
     order_qt_sql1 """
         select convert_tz(dt, '+00:00', IF(property_value IS NULL, '+00:00', 
property_value)) from cvt_tz
     """
+    sql "set debug_skip_fold_constant=true;"
+    qt_spring_gp_with_micro_sec "select convert_tz('2021-03-28 
02:30:00.00323', 'Europe/Paris','UTC');"
 }


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

Reply via email to