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

Gabriel39 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 ecae3c4a4a6 [fix](iceberg) Support fractional timestamps in time 
travel (#67705)
ecae3c4a4a6 is described below

commit ecae3c4a4a6c7c6503b43ff3b4679cb22a5b4641
Author: Gabriel <[email protected]>
AuthorDate: Thu Sep 10 11:20:22 2026 +0800

    [fix](iceberg) Support fractional timestamps in time travel (#67705)
    
    ### What problem does this PR solve?
    
    Iceberg `FOR TIME AS OF` rejected timestamps containing fractional
    seconds, even though the snapshots metadata exposes `committed_at` with
    fractional precision. Truncating the value to whole seconds can also
    select the wrong snapshot when multiple snapshots are committed within
    the same second.
    
    ### What is changed and how does it work?
    
    - Accept an optional 1-9 digit fractional-second component in Iceberg
    time-travel timestamp literals while preserving whole-second
    compatibility.
    - Keep session time-zone interpretation unchanged.
    - Add unit coverage for millisecond and microsecond-formatted literals.
    - Update the Iceberg time-travel regression matrix to feed the
    fractional `committed_at` value back into `FOR TIME AS OF` directly.
    
    ### Check List
    
    - [x] Unit test: `IcebergTimeUtilsTest` (6 tests)
    - [x] FE Checkstyle
    - [x] `git diff --check`
---
 .../doris/connector/iceberg/IcebergTimeUtils.java  | 22 ++++++++++++++--------
 .../connector/iceberg/IcebergTimeUtilsTest.java    |  9 +++++++++
 .../test_iceberg_schema_time_travel_matrix.groovy  |  4 ++--
 3 files changed, 25 insertions(+), 10 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergTimeUtils.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergTimeUtils.java
index 34f4f8c8d79..fe561834eb1 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergTimeUtils.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergTimeUtils.java
@@ -24,7 +24,9 @@ import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
 import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
 import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
@@ -44,9 +46,14 @@ public final class IcebergTimeUtils {
     // Doris overrides (CST/PRC -> Asia/Shanghai, UTC/GMT -> UTC = TimeUtils 
DEFAULT/UTC_TIME_ZONE).
     private static final Map<String, String> TIME_ZONE_ALIAS_MAP;
 
-    // Byte-parity with legacy TimeUtils.DATETIME_FORMAT (= 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
-    // the formatter TimeUtils.timeStringToLong uses for a non-digital FOR 
TIME AS OF datetime string.
-    private static final DateTimeFormatter DATETIME_FORMAT = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    // Iceberg's committed_at value may include fractional seconds, so the 
time-travel parser must round-trip
+    // that value while preserving compatibility with existing whole-second 
literals.
+    private static final DateTimeFormatter DATETIME_FORMAT = new 
DateTimeFormatterBuilder()
+            .appendPattern("yyyy-MM-dd HH:mm:ss")
+            .optionalStart()
+            .appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
+            .optionalEnd()
+            .toFormatter();
 
     // Byte-parity with legacy TimeUtils.DATETIME_MS_FORMAT (= 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")),
     // the formatter TimeUtils.msTimeStringToLong uses. The 
rollback_to_timestamp EXECUTE action parses its
@@ -88,11 +95,10 @@ public final class IcebergTimeUtils {
     }
 
     /**
-     * Parses a {@code FOR TIME AS OF} datetime string to epoch-millis in 
{@code zone}, byte-faithful to legacy
-     * {@code TimeUtils.timeStringToLong(value, sessionTZ)} (parse {@code 
yyyy-MM-dd HH:mm:ss} as a local
-     * date-time, then interpret it in the session zone). Legacy returned 
{@code -1} on a parse failure and the
-     * caller ({@code IcebergUtils.getQuerySpecSnapshot}) turned that into a 
{@code DateTimeException}; we throw
-     * it directly (fail loud — a parse error is a user mistake, not a 
not-found).
+     * Parses a {@code FOR TIME AS OF} datetime string with optional 
fractional seconds to epoch-millis in
+     * {@code zone}. Legacy returned {@code -1} on a parse failure and the 
caller
+     * ({@code IcebergUtils.getQuerySpecSnapshot}) turned that into a {@code 
DateTimeException}; we throw it
+     * directly (fail loud — a parse error is a user mistake, not a not-found).
      */
     static long datetimeToMillis(String value, ZoneId zone) {
         try {
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTimeUtilsTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTimeUtilsTest.java
index 86a92fb733a..c5461f85807 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTimeUtilsTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergTimeUtilsTest.java
@@ -52,6 +52,15 @@ public class IcebergTimeUtilsTest {
                 IcebergTimeUtils.datetimeToMillis("2023-06-15 10:30:00", 
ZoneOffset.UTC));
     }
 
+    @Test
+    public void datetimeToMillisAcceptsFractionalSeconds() {
+        long expected = 
Instant.parse("2023-06-15T10:30:00.526Z").toEpochMilli();
+        Assertions.assertEquals(expected,
+                IcebergTimeUtils.datetimeToMillis("2023-06-15 10:30:00.526", 
ZoneOffset.UTC));
+        Assertions.assertEquals(expected,
+                IcebergTimeUtils.datetimeToMillis("2023-06-15 
10:30:00.526000", ZoneOffset.UTC));
+    }
+
     @Test
     public void datetimeToMillisFailsLoudOnMalformedString() {
         // WHY: a malformed datetime is a user mistake; legacy returned -1 and 
the caller threw
diff --git 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_schema_time_travel_matrix.groovy
 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_schema_time_travel_matrix.groovy
index b6fa84dc9bf..dffbc236ea7 100644
--- 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_schema_time_travel_matrix.groovy
+++ 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_schema_time_travel_matrix.groovy
@@ -457,7 +457,7 @@ suite("test_iceberg_schema_time_travel_matrix",
 
         // Scenario TC02/T03/T04: complex-field time travel uses the 
pre-change nested schema.
         List<List<Object>> dorisNestedCp0Time = sql("""
-            select date_format(date_add(committed_at, interval 1 second), 
'%Y-%m-%d %H:%i:%s'),
+            select date_format(committed_at, '%Y-%m-%d %H:%i:%s.%f'),
                    cast(unix_timestamp(committed_at) * 1000 + 999 as bigint)
             from ${dorisNestedTable}\$snapshots
             where snapshot_id = ${dorisNestedCp0}
@@ -561,7 +561,7 @@ suite("test_iceberg_schema_time_travel_matrix",
 
         // Scenario T03/T04: string and numeric epoch-millis time travel both 
resolve the snapshot.
         List<List<Object>> cp0TimeRows = sql("""
-            select date_format(date_add(committed_at, interval 1 second), 
'%Y-%m-%d %H:%i:%s'),
+            select date_format(committed_at, '%Y-%m-%d %H:%i:%s.%f'),
                    cast(unix_timestamp(committed_at) * 1000 + 999 as bigint)
             from ${topTable}\$snapshots
             where snapshot_id = ${topCp0}


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

Reply via email to