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

nzw921rx pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 0145209931 [Fix][Connector-V2] Fix Arrow microsecond timestamp with 
timezone conversion (#12616)
0145209931 is described below

commit 0145209931a4fe6795bbcf64a827f0f75302a624
Author: nankeChen75 <[email protected]>
AuthorDate: Mon Oct 26 21:15:29 2026 +0800

    [Fix][Connector-V2] Fix Arrow microsecond timestamp with timezone 
conversion (#12616)
    
    Co-authored-by: Xiaonan Chen <[email protected]>
---
 .../arrow/converter/TimeStampMicroTZConverter.java | 61 ++++++++++++++++++
 .../arrow/converter/TimeStampNanoTZConverter.java  | 59 ++++++++++++++++++
 .../arrow/ArrowToSeatunnelRowReaderTest.java       | 72 +++++++++++++++++++++-
 .../source/arrow/SeaTunnelDataTypeHolder.java      |  2 +
 4 files changed, 193 insertions(+), 1 deletion(-)

diff --git 
a/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/converter/TimeStampMicroTZConverter.java
 
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/converter/TimeStampMicroTZConverter.java
new file mode 100644
index 0000000000..6f4342e6ba
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/converter/TimeStampMicroTZConverter.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package 
org.apache.seatunnel.connectors.seatunnel.common.source.arrow.converter;
+
+import 
org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampMicroTZVector;
+import org.apache.seatunnel.shade.org.apache.arrow.vector.types.Types;
+
+import com.google.auto.service.AutoService;
+
+import java.time.Instant;
+import java.time.ZoneId;
+
+/**
+ * Converts Arrow {@code TIMESTAMPMICROTZ} values from epoch microseconds to 
{@link
+ * java.time.LocalDateTime} using the time zone declared by the Arrow field.
+ *
+ * <p>{@link Math#floorDiv(long, long)} and {@link Math#floorMod(long, long)} 
ensure that timestamps
+ * before the Unix epoch are converted correctly.
+ */
+@AutoService(Converter.class)
+public class TimeStampMicroTZConverter implements 
Converter<TimeStampMicroTZVector> {
+    @Override
+    public Object convert(int rowIndex, TimeStampMicroTZVector fieldVector) {
+        if (fieldVector == null || fieldVector.isNull(rowIndex)) {
+            return null;
+        }
+
+        long epochMicro = fieldVector.getObject(rowIndex);
+        long epochSecond = Math.floorDiv(epochMicro, 1_000_000L);
+        long microOfSecond = Math.floorMod(epochMicro, 1_000_000L);
+
+        String timeZone = fieldVector.getTimeZone();
+        ZoneId zoneId =
+                timeZone == null || timeZone.isEmpty()
+                        ? ZoneId.systemDefault()
+                        : ZoneId.of(timeZone);
+
+        return Instant.ofEpochSecond(epochSecond, microOfSecond * 1_000L)
+                .atZone(zoneId)
+                .toLocalDateTime();
+    }
+
+    @Override
+    public boolean support(Types.MinorType type) {
+        return Types.MinorType.TIMESTAMPMICROTZ == type;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/converter/TimeStampNanoTZConverter.java
 
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/converter/TimeStampNanoTZConverter.java
new file mode 100644
index 0000000000..2d4a925e4a
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/converter/TimeStampNanoTZConverter.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+package 
org.apache.seatunnel.connectors.seatunnel.common.source.arrow.converter;
+
+import 
org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampNanoTZVector;
+import org.apache.seatunnel.shade.org.apache.arrow.vector.types.Types;
+
+import com.google.auto.service.AutoService;
+
+import java.time.Instant;
+import java.time.ZoneId;
+
+/**
+ * Converts Arrow {@code TIMESTAMPNANOTZ} values from epoch nanoseconds to 
{@link
+ * java.time.LocalDateTime} using the time zone declared by the Arrow field.
+ *
+ * <p>{@link Math#floorDiv(long, long)} and {@link Math#floorMod(long, long)} 
ensure that timestamps
+ * before the Unix epoch are converted correctly.
+ */
+@AutoService(Converter.class)
+public class TimeStampNanoTZConverter implements 
Converter<TimeStampNanoTZVector> {
+    @Override
+    public Object convert(int rowIndex, TimeStampNanoTZVector fieldVector) {
+        if (fieldVector == null || fieldVector.isNull(rowIndex)) {
+            return null;
+        }
+
+        long epochNano = fieldVector.getObject(rowIndex);
+        long epochSecond = Math.floorDiv(epochNano, 1_000_000_000L);
+        long nanoOfSecond = Math.floorMod(epochNano, 1_000_000_000L);
+
+        String timeZone = fieldVector.getTimeZone();
+        ZoneId zoneId =
+                timeZone == null || timeZone.isEmpty()
+                        ? ZoneId.systemDefault()
+                        : ZoneId.of(timeZone);
+
+        return Instant.ofEpochSecond(epochSecond, 
nanoOfSecond).atZone(zoneId).toLocalDateTime();
+    }
+
+    @Override
+    public boolean support(Types.MinorType type) {
+        return Types.MinorType.TIMESTAMPNANOTZ == type;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/ArrowToSeatunnelRowReaderTest.java
 
b/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/ArrowToSeatunnelRowReaderTest.java
index 604c10110e..87c7b62db7 100644
--- 
a/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/ArrowToSeatunnelRowReaderTest.java
+++ 
b/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/ArrowToSeatunnelRowReaderTest.java
@@ -34,8 +34,10 @@ import 
org.apache.seatunnel.shade.org.apache.arrow.vector.IntVector;
 import org.apache.seatunnel.shade.org.apache.arrow.vector.LargeVarCharVector;
 import org.apache.seatunnel.shade.org.apache.arrow.vector.SmallIntVector;
 import org.apache.seatunnel.shade.org.apache.arrow.vector.TimeMicroVector;
+import 
org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampMicroTZVector;
 import org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampMicroVector;
 import 
org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampMilliTZVector;
+import 
org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampNanoTZVector;
 import org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampSecTZVector;
 import org.apache.seatunnel.shade.org.apache.arrow.vector.TimeStampSecVector;
 import org.apache.seatunnel.shade.org.apache.arrow.vector.TinyIntVector;
@@ -69,6 +71,7 @@ import java.io.ByteArrayOutputStream;
 import java.math.BigDecimal;
 import java.nio.channels.Channels;
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
@@ -89,6 +92,19 @@ public class ArrowToSeatunnelRowReaderTest {
     private static final LocalDateTime localDateTime =
             LocalDateTime.parse(
                     "2025-02-15 02:21:23", 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+    private static final LocalDateTime localDateTimeMicro =
+            LocalDateTime.parse(
+                    "2025-02-15 02:21:23.123456",
+                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS"));
+    private static final LocalDateTime localDateTimeNano =
+            LocalDateTime.parse(
+                    "2025-02-15 02:21:23.123456789",
+                    DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss.SSSSSSSSS"));
+
+    private static final ZoneId timestampTzZoneId =
+            ZoneId.systemDefault().equals(ZoneId.of("Asia/Shanghai"))
+                    ? ZoneId.of("America/New_York")
+                    : ZoneId.of("Asia/Shanghai");
 
     private static final List<String> stringData = new ArrayList<>();
     private static final List<Byte> byteData = new ArrayList<>();
@@ -127,6 +143,8 @@ public class ArrowToSeatunnelRowReaderTest {
         seaTunnelDataTypeHolder.add(new 
SeaTunnelDataTypeHolder("timestampSec", 0));
         seaTunnelDataTypeHolder.add(new 
SeaTunnelDataTypeHolder("timestampSecTz", 0));
         seaTunnelDataTypeHolder.add(new SeaTunnelDataTypeHolder("map", 0));
+        seaTunnelDataTypeHolder.add(new 
SeaTunnelDataTypeHolder("timestampMicroTz", 0));
+        seaTunnelDataTypeHolder.add(new 
SeaTunnelDataTypeHolder("timestampNanoTz", 0));
     }
 
     private static VectorSchemaRoot buildVectorSchemaRoot(
@@ -161,6 +179,22 @@ public class ArrowToSeatunnelRowReaderTest {
                                     new ArrowType.Timestamp(
                                             TimeUnit.MILLISECOND, 
ZoneId.systemDefault().getId())),
                             rootAllocator));
+            // Microsecond timestamp with timezone
+            vectors.add(
+                    new TimeStampMicroTZVector(
+                            Field.nullable(
+                                    "timestampMicroTz",
+                                    new ArrowType.Timestamp(
+                                            TimeUnit.MICROSECOND, 
timestampTzZoneId.getId())),
+                            rootAllocator));
+            // Nanosecond timestamp with timezone
+            vectors.add(
+                    new TimeStampNanoTZVector(
+                            Field.nullable(
+                                    "timestampNanoTz",
+                                    new ArrowType.Timestamp(
+                                            TimeUnit.NANOSECOND, 
timestampTzZoneId.getId())),
+                            rootAllocator));
             vectors.add(new TimeMicroVector("time", rootAllocator));
             vectors.add(new DateMilliVector("date1", rootAllocator));
             vectors.add(new DateDayVector("date2", rootAllocator));
@@ -186,6 +220,12 @@ public class ArrowToSeatunnelRowReaderTest {
         long epochMilli = 
localDateTime.atZone(zoneId).toInstant().toEpochMilli();
         long epochSecond = 
localDateTime.atZone(zoneId).toInstant().getEpochSecond();
 
+        Instant microInstant = 
localDateTimeMicro.atZone(timestampTzZoneId).toInstant();
+        long epochMicro =
+                microInstant.getEpochSecond() * 1_000_000L + 
microInstant.getNano() / 1_000;
+        Instant nanoInstant = 
localDateTimeNano.atZone(timestampTzZoneId).toInstant();
+        long epochNano = nanoInstant.getEpochSecond() * 1_000_000_000L + 
nanoInstant.getNano();
+
         byte byteStart = 'a';
 
         // setVectorValue
@@ -239,6 +279,21 @@ public class ArrowToSeatunnelRowReaderTest {
                             ((TimeStampSecVector) vector).setSafe(i, 
epochSecond);
                         } else if (vector instanceof TimeStampSecTZVector) {
                             ((TimeStampSecTZVector) vector).setSafe(i, 
epochSecond);
+                        } else if (vector instanceof TimeStampMicroTZVector) {
+                            TimeStampMicroTZVector timestampVector =
+                                    (TimeStampMicroTZVector) vector;
+                            if (i == 0) {
+                                timestampVector.setNull(i);
+                            } else {
+                                timestampVector.setSafe(i, epochMicro);
+                            }
+                        } else if (vector instanceof TimeStampNanoTZVector) {
+                            TimeStampNanoTZVector timestampVector = 
(TimeStampNanoTZVector) vector;
+                            if (i == 0) {
+                                timestampVector.setNull(i);
+                            } else {
+                                timestampVector.setSafe(i, epochNano);
+                            }
                         }
                     }
                 });
@@ -414,11 +469,26 @@ public class ArrowToSeatunnelRowReaderTest {
                     rows.stream().map(s -> 
s.getField(19)).distinct().collect(Collectors.toList());
             Assertions.assertEquals(
                     Collections.singletonList(localDateTime), 
actualTimestampSecTzData);
-
             // todo check map
             // The java api has problems building MapVectors,and there are no 
examples on the
             // official website
             // @see https://github.com/apache/arrow/issues/44664
+
+            // Check null microsecond timestamp with timezone
+            Assertions.assertNull(rows.get(0).getField(21));
+
+            // Check non-null microsecond timestamps with timezone
+            for (int i = 1; i < rows.size(); i++) {
+                Assertions.assertEquals(localDateTimeMicro, 
rows.get(i).getField(21));
+            }
+
+            // Check null nanosecond timestamp with timezone
+            Assertions.assertNull(rows.get(0).getField(22));
+
+            // Check non-null nanosecond timestamps with timezone
+            for (int i = 1; i < rows.size(); i++) {
+                Assertions.assertEquals(localDateTimeNano, 
rows.get(i).getField(22));
+            }
         }
     }
 
diff --git 
a/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/SeaTunnelDataTypeHolder.java
 
b/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/SeaTunnelDataTypeHolder.java
index 308ba54647..33e14f8ad7 100644
--- 
a/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/SeaTunnelDataTypeHolder.java
+++ 
b/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/source/arrow/SeaTunnelDataTypeHolder.java
@@ -67,6 +67,8 @@ public class SeaTunnelDataTypeHolder {
             case "timestamp2":
             case "timestampSec":
             case "timestampSecTz":
+            case "timestampMicroTz":
+            case "timestampNanoTz":
                 return LocalTimeType.LOCAL_DATE_TIME_TYPE;
             case "time":
                 return LocalTimeType.LOCAL_TIME_TYPE;

Reply via email to