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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 7ed09f49eaf [fix](iceberg) Support fractional timestamps in time 
travel (#67704)
7ed09f49eaf is described below

commit 7ed09f49eafb4f229cdafb8a3c86bbd882dd5f31
Author: Gabriel <[email protected]>
AuthorDate: Thu Sep 10 11:08:20 2026 +0800

    [fix](iceberg) Support fractional timestamps in time travel (#67704)
    
    ### 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 the
    Iceberg-specific time-travel parser while leaving other `TimeUtils`
    consumers unchanged.
    - Keep session time-zone interpretation unchanged.
    - Add unit coverage that distinguishes two snapshots committed within
    the same second.
    - 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: `IcebergUtilsTest` (38 tests)
    - [x] FE Checkstyle
    - [x] `git diff --check`
---
 .../doris/datasource/iceberg/IcebergUtils.java      | 21 ++++++++++++++++++++-
 .../doris/datasource/iceberg/IcebergUtilsTest.java  | 16 +++++++++++++---
 .../test_iceberg_schema_time_travel_matrix.groovy   |  4 ++--
 3 files changed, 35 insertions(+), 6 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
index c60f64c6f9c..a43ce9dc9d2 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java
@@ -145,6 +145,8 @@ import java.time.Month;
 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.time.temporal.TemporalAccessor;
 import java.util.ArrayList;
@@ -256,6 +258,14 @@ public class IcebergUtils {
     public static final String ICEBERG_LAST_UPDATED_SEQUENCE_NUMBER_COL = 
"_last_updated_sequence_number";
 
     private static final Pattern SNAPSHOT_ID = Pattern.compile("\\d+");
+    // 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 TIME_TRAVEL_DATETIME_FORMAT = new 
DateTimeFormatterBuilder()
+            .appendPattern("yyyy-MM-dd HH:mm:ss")
+            .optionalStart()
+            .appendFraction(ChronoField.NANO_OF_SECOND, 1, 9, true)
+            .optionalEnd()
+            .toFormatter();
 
     public static boolean hasIcebergCatalogFormatVersion(Map<String, String> 
catalogProperties) {
         return 
catalogProperties.containsKey(CatalogProperties.TABLE_OVERRIDE_PREFIX + 
TableProperties.FORMAT_VERSION)
@@ -1857,7 +1867,7 @@ public class IcebergUtils {
                 SnapshotUtil.schemaFor(table, value).schemaId()
             );
         } else {
-            long timestamp = TimeUtils.timeStringToLong(value, 
TimeUtils.getTimeZone());
+            long timestamp = timeTravelTimestampToLong(value);
             if (timestamp < 0) {
                 throw new DateTimeException("can't parse time: " + value);
             }
@@ -1870,6 +1880,15 @@ public class IcebergUtils {
         }
     }
 
+    private static long timeTravelTimestampToLong(String value) {
+        try {
+            return LocalDateTime.parse(value, TIME_TRAVEL_DATETIME_FORMAT)
+                    
.atZone(TimeUtils.getTimeZone().toZoneId()).toInstant().toEpochMilli();
+        } catch (DateTimeParseException e) {
+            return -1;
+        }
+    }
+
     public static boolean isIcebergBranchOrTag(Optional<TableScanParams> 
scanParams) {
         if (scanParams == null || !scanParams.isPresent()) {
             return false;
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
index 5ba783d1576..393d60cf6f5 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/iceberg/IcebergUtilsTest.java
@@ -1007,6 +1007,10 @@ public class IcebergUtilsTest {
         Mockito.when(table.snapshot(3)).thenReturn(s3);
         Snapshot s4 = mockSnapshot(4, 1);
         Mockito.when(table.snapshot(4)).thenReturn(s4);
+        Snapshot s5 = mockSnapshot(5, 2);
+        Mockito.when(table.snapshot(5)).thenReturn(s5);
+        Snapshot s6 = mockSnapshot(6, 2);
+        Mockito.when(table.snapshot(6)).thenReturn(s6);
 
         // init history for snapshots
         List<HistoryEntry> history = new ArrayList<>();
@@ -1014,6 +1018,8 @@ public class IcebergUtilsTest {
         history.add(mockHistory(2, "2025-05-01 22:34:56"));
         history.add(mockHistory(3, "2025-05-02 12:34:56"));
         history.add(mockHistory(4, "2025-05-03 12:34:56"));
+        history.add(mockHistory(5, LocalDateTime.of(2025, 5, 4, 12, 34, 56, 
125_000_000)));
+        history.add(mockHistory(6, LocalDateTime.of(2025, 5, 4, 12, 34, 56, 
526_000_000)));
         Mockito.when(table.history()).thenReturn(history);
 
         // create some refs
@@ -1127,6 +1133,8 @@ public class IcebergUtilsTest {
         assertQuerySpecSnapshotByTimeOf(table, "2025-05-02 11:34:56", 2, 0, 
null);
         assertQuerySpecSnapshotByTimeOf(table, "2025-05-02 12:34:56", 3, 1, 
null);
         assertQuerySpecSnapshotByTimeOf(table, "2025-05-03 12:34:56", 4, 1, 
null);
+        assertQuerySpecSnapshotByTimeOf(table, "2025-05-04 12:34:56.125", 5, 
2, null);
+        assertQuerySpecSnapshotByTimeOf(table, "2025-05-04 12:34:56.526000", 
6, 2, null);
 
         // query invalid time format
         Assert.assertThrows(
@@ -1149,11 +1157,13 @@ public class IcebergUtilsTest {
     }
 
     private HistoryEntry mockHistory(long snapshotId, String time) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss");
+        return mockHistory(snapshotId, LocalDateTime.parse(time, formatter));
+    }
+
+    private HistoryEntry mockHistory(long snapshotId, LocalDateTime dateTime) {
         HistoryEntry historyEntry = Mockito.mock(HistoryEntry.class);
         Mockito.when(historyEntry.snapshotId()).thenReturn(snapshotId);
-
-        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss");
-        LocalDateTime dateTime = LocalDateTime.parse(time, formatter);
         long millis = 
dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
 
         Mockito.when(historyEntry.timestampMillis()).thenReturn(millis);
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 f92f3b83c59..56f06cf5769 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
@@ -458,7 +458,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}
@@ -559,7 +559,7 @@ suite("test_iceberg_schema_time_travel_matrix",
 
         // Scenario T03/T04: validate string time travel and reject 
unsupported epoch millis.
         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