You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@paimon.apache.org by lz...@apache.org on 2023/05/26 08:32:30 UTC

[incubator-paimon] branch master updated: [core] paimon format avro support Timestamp LTZ (#1171)

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

lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 965620a5e [core] paimon format avro support Timestamp LTZ (#1171)
965620a5e is described below

commit 965620a5ec96772dac0f414d4510bfbf32e3cee0
Author: legendtkl <ta...@gmail.com>
AuthorDate: Fri May 26 16:32:24 2023 +0800

    [core] paimon format avro support Timestamp LTZ (#1171)
---
 .../paimon/format/avro/AvroSchemaConverter.java    | 24 ++++++++-
 .../format/avro/AvroToRowDataConverters.java       | 49 ++++++++++++++++---
 .../format/avro/RowDataToAvroConverters.java       | 34 ++++++++++++-
 .../paimon/format/avro/AvroBulkFormatTest.java     | 49 +++++++++++++++----
 .../format/avro/AvroBulkFormatTestUtils.java       | 18 ++++++-
 .../paimon/format/avro/AvroFileFormatTest.java     | 17 ++-----
 .../format/avro/AvroToRowDataConvertersTest.java   | 47 ++++++++++++++++++
 .../format/avro/RowDataToAvroConvertersTest.java   | 57 ++++++++++++++++++++++
 .../java/org/apache/paimon/hive/HiveTypeUtils.java |  1 +
 .../org/apache/paimon/hive/HiveTypeUtilsTest.java  | 11 ++---
 10 files changed, 265 insertions(+), 42 deletions(-)

diff --git a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaConverter.java b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaConverter.java
index 86fc18110..bfca9006a 100644
--- a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaConverter.java
+++ b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaConverter.java
@@ -23,6 +23,7 @@ import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypeRoot;
 import org.apache.paimon.types.DecimalType;
 import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.LocalZonedTimestampType;
 import org.apache.paimon.types.MapType;
 import org.apache.paimon.types.MultisetType;
 import org.apache.paimon.types.RowType;
@@ -101,15 +102,36 @@ public class AvroSchemaConverter {
                 org.apache.avro.LogicalType avroLogicalType;
                 if (precision <= 3) {
                     avroLogicalType = LogicalTypes.timestampMillis();
+                } else if (precision <= 6) {
+                    avroLogicalType = LogicalTypes.timestampMicros();
                 } else {
                     throw new IllegalArgumentException(
                             "Avro does not support TIMESTAMP type "
                                     + "with precision: "
                                     + precision
-                                    + ", it only supports precision less than 3.");
+                                    + ", it only supports precision less than 6.");
                 }
                 Schema timestamp = avroLogicalType.addToSchema(SchemaBuilder.builder().longType());
                 return nullable ? nullableSchema(timestamp) : timestamp;
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                final LocalZonedTimestampType localTimestampType =
+                        (LocalZonedTimestampType) dataType;
+                precision = localTimestampType.getPrecision();
+                org.apache.avro.LogicalType localTimestampLogicalType;
+                if (precision <= 3) {
+                    localTimestampLogicalType = LogicalTypes.localTimestampMillis();
+                } else if (precision <= 6) {
+                    localTimestampLogicalType = LogicalTypes.localTimestampMicros();
+                } else {
+                    throw new IllegalArgumentException(
+                            "Avro does not support TIMESTAMP type "
+                                    + "with precision: "
+                                    + precision
+                                    + ", it only supports precision less than 6.");
+                }
+                Schema localTimestampSchema =
+                        localTimestampLogicalType.addToSchema(SchemaBuilder.builder().longType());
+                return nullable ? nullableSchema(localTimestampSchema) : localTimestampSchema;
             case DATE:
                 // use int to represents Date
                 Schema date = LogicalTypes.date().addToSchema(SchemaBuilder.builder().intType());
diff --git a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroToRowDataConverters.java b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroToRowDataConverters.java
index cf15f4f41..3e2e11700 100644
--- a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroToRowDataConverters.java
+++ b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroToRowDataConverters.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.format.avro;
 
+import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.Decimal;
 import org.apache.paimon.data.GenericArray;
@@ -30,7 +31,9 @@ import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.DecimalType;
+import org.apache.paimon.types.LocalZonedTimestampType;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.TimestampType;
 
 import org.apache.avro.generic.GenericFixed;
 import org.apache.avro.generic.GenericRecord;
@@ -97,7 +100,8 @@ public class AvroToRowDataConverters {
     }
 
     /** Creates a runtime converter which assuming input object is not null. */
-    private static AvroToRowDataConverter createConverter(DataType type) {
+    @VisibleForTesting
+    static AvroToRowDataConverter createConverter(DataType type) {
         switch (type.getTypeRoot()) {
             case TINYINT:
                 return avroObject -> ((Integer) avroObject).byteValue();
@@ -114,7 +118,23 @@ public class AvroToRowDataConverters {
             case TIME_WITHOUT_TIME_ZONE:
                 return AvroToRowDataConverters::convertToTime;
             case TIMESTAMP_WITHOUT_TIME_ZONE:
-                return AvroToRowDataConverters::convertToTimestamp;
+                int precision = ((TimestampType) type).getPrecision();
+                if (precision <= 3) {
+                    return AvroToRowDataConverters::convertToTimestampFromMillis;
+                } else if (precision <= 6) {
+                    return AvroToRowDataConverters::convertToTimestampFromMicros;
+                } else {
+                    throw new UnsupportedOperationException("Unsupported precision: " + precision);
+                }
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                precision = ((LocalZonedTimestampType) type).getPrecision();
+                if (precision <= 3) {
+                    return AvroToRowDataConverters::convertToTimestampFromMillis;
+                } else if (precision <= 6) {
+                    return AvroToRowDataConverters::convertToTimestampFromMicros;
+                } else {
+                    throw new UnsupportedOperationException("Unsupported precision: " + precision);
+                }
             case CHAR:
             case VARCHAR:
                 return avroObject -> BinaryString.fromString(avroObject.toString());
@@ -186,22 +206,35 @@ public class AvroToRowDataConverters {
         };
     }
 
-    private static Timestamp convertToTimestamp(Object object) {
-        final long millis;
+    private static Timestamp convertToTimestampFromMillis(Object object) {
+        return convertToTimestamp(object, 3);
+    }
+
+    private static Timestamp convertToTimestampFromMicros(Object object) {
+        return convertToTimestamp(object, 6);
+    }
+
+    @VisibleForTesting
+    static Timestamp convertToTimestamp(Object object, int precision) {
         if (object instanceof Long) {
-            millis = (Long) object;
+            if (precision <= 3) {
+                return Timestamp.fromEpochMillis((Long) object);
+            } else if (precision <= 6) {
+                return Timestamp.fromMicros((Long) object);
+            } else {
+                throw new UnsupportedOperationException("Unsupported precision: " + precision);
+            }
         } else if (object instanceof Instant) {
-            millis = ((Instant) object).toEpochMilli();
+            return Timestamp.fromInstant((Instant) object);
         } else {
             JodaConverter jodaConverter = JodaConverter.getConverter();
             if (jodaConverter != null) {
-                millis = jodaConverter.convertTimestamp(object);
+                return Timestamp.fromEpochMillis(jodaConverter.convertTimestamp(object));
             } else {
                 throw new IllegalArgumentException(
                         "Unexpected object type for TIMESTAMP logical type. Received: " + object);
             }
         }
-        return Timestamp.fromEpochMillis(millis);
     }
 
     private static int convertToDate(Object object) {
diff --git a/paimon-format/src/main/java/org/apache/paimon/format/avro/RowDataToAvroConverters.java b/paimon-format/src/main/java/org/apache/paimon/format/avro/RowDataToAvroConverters.java
index a89e8d070..76bcb380c 100644
--- a/paimon-format/src/main/java/org/apache/paimon/format/avro/RowDataToAvroConverters.java
+++ b/paimon-format/src/main/java/org/apache/paimon/format/avro/RowDataToAvroConverters.java
@@ -28,6 +28,8 @@ import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.RowType;
 
+import org.apache.avro.LogicalType;
+import org.apache.avro.LogicalTypes;
 import org.apache.avro.Schema;
 import org.apache.avro.generic.GenericData;
 import org.apache.avro.generic.GenericRecord;
@@ -142,7 +144,37 @@ public class RowDataToAvroConverters {
 
                             @Override
                             public Object convert(Schema schema, Object object) {
-                                return ((Timestamp) object).toInstant().toEpochMilli();
+                                LogicalType logicalType = schema.getLogicalType();
+
+                                if (logicalType instanceof LogicalTypes.TimestampMillis) {
+                                    return ((Timestamp) object).toInstant().toEpochMilli();
+                                } else if (logicalType instanceof LogicalTypes.TimestampMicros) {
+                                    return ((Timestamp) object).toMicros();
+                                } else {
+                                    throw new UnsupportedOperationException(
+                                            "Unsupported timestamp type: " + logicalType);
+                                }
+                            }
+                        };
+                break;
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                converter =
+                        new RowDataToAvroConverter() {
+                            private static final long serialVersionUID = 1L;
+
+                            @Override
+                            public Object convert(Schema schema, Object object) {
+                                LogicalType logicalType = schema.getLogicalType();
+
+                                if (logicalType instanceof LogicalTypes.LocalTimestampMillis) {
+                                    return ((Timestamp) object).toInstant().toEpochMilli();
+                                } else if (logicalType
+                                        instanceof LogicalTypes.LocalTimestampMicros) {
+                                    return ((Timestamp) object).toMicros();
+                                } else {
+                                    throw new UnsupportedOperationException(
+                                            "Unsupported timestamp type: " + logicalType);
+                                }
                             }
                         };
                 break;
diff --git a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTest.java b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTest.java
index 054a3369b..b73cde5cc 100644
--- a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTest.java
+++ b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTest.java
@@ -21,6 +21,7 @@ package org.apache.paimon.format.avro;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.Timestamp;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.reader.RecordReader;
@@ -49,35 +50,63 @@ import static org.assertj.core.api.Assertions.assertThat;
 /** Tests for {@link AbstractAvroBulkFormat}. */
 class AvroBulkFormatTest {
 
+    private static final long TIMESTAMP = System.currentTimeMillis();
+
     private static final List<InternalRow> TEST_DATA =
             Arrays.asList(
                     // -------- batch 0, block start 232 --------
                     GenericRow.of(
                             BinaryString.fromString("AvroBulk"),
-                            BinaryString.fromString("FormatTest")),
+                            BinaryString.fromString("FormatTest"),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123)),
                     GenericRow.of(
-                            BinaryString.fromString("Apache"), BinaryString.fromString("Paimon")),
+                            BinaryString.fromString("Apache"),
+                            BinaryString.fromString("Paimon"),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123)),
                     GenericRow.of(
                             BinaryString.fromString(
                                     "永和九年,岁在癸丑,暮春之初,会于会稽山阴之兰亭,修禊事也。群贤毕至,少"
                                             + "长咸集。此地有崇山峻岭,茂林修竹,又有清流激湍,映带左右。引"
                                             + "以为流觞曲水,列坐其次。虽无丝竹管弦之盛,一觞一咏,亦足以畅"
                                             + "叙幽情。"),
-                            BinaryString.fromString("")),
-                    // -------- batch 1, block start 593 --------
+                            BinaryString.fromString(""),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123)),
+                    // -------- batch 1, block start 689 --------
                     GenericRow.of(
-                            BinaryString.fromString("File"), BinaryString.fromString("Format")),
+                            BinaryString.fromString("File"),
+                            BinaryString.fromString("Format"),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123)),
                     GenericRow.of(
                             null,
                             BinaryString.fromString(
-                                    "This is a string with English, 中文 and even 🍎🍌🍑🥝🍍🥭🍐")),
-                    // -------- batch 2, block start 705 --------
+                                    "This is a string with English, 中文 and even 🍎🍌🍑🥝🍍🥭🍐"),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123)),
+                    // -------- batch 2, block start 1147 --------
                     GenericRow.of(
                             BinaryString.fromString("block with"),
-                            BinaryString.fromString("only one record"))
-                    // -------- file length 752 --------
+                            BinaryString.fromString("only one record"),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123),
+                            Timestamp.fromEpochMillis(TIMESTAMP),
+                            Timestamp.fromMicros(TIMESTAMP * 1000 + 123))
+                    // -------- file length 1323 --------
                     );
-    private static final List<Long> BLOCK_STARTS = Arrays.asList(233L, 595L, 707L);
+    private static final List<Long> BLOCK_STARTS = Arrays.asList(689L, 1147L, 1323L);
 
     private File tmpFile;
 
diff --git a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTestUtils.java b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTestUtils.java
index a8901949c..b68a6379b 100644
--- a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTestUtils.java
+++ b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBulkFormatTestUtils.java
@@ -35,8 +35,22 @@ public class AvroBulkFormatTestUtils {
             (RowType)
                     RowType.builder()
                             .fields(
-                                    new DataType[] {DataTypes.STRING(), DataTypes.STRING()},
-                                    new String[] {"a", "b"})
+                                    new DataType[] {
+                                        DataTypes.STRING(),
+                                        DataTypes.STRING(),
+                                        DataTypes.TIMESTAMP(3),
+                                        DataTypes.TIMESTAMP(6),
+                                        DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(3),
+                                        DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(6),
+                                    },
+                                    new String[] {
+                                        "a",
+                                        "b",
+                                        "timestamp_millis",
+                                        "timestamp_micros",
+                                        "local_timestamp_millis",
+                                        "local_timestamp_micros"
+                                    })
                             .build()
                             .notNull();
 
diff --git a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFileFormatTest.java b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFileFormatTest.java
index 525f2778b..0aa15f7f2 100644
--- a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFileFormatTest.java
+++ b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFileFormatTest.java
@@ -23,7 +23,6 @@ import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
 
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
@@ -57,6 +56,11 @@ public class AvroFileFormatTest {
         dataFields.add(new DataField(index++, "timestamp_type", DataTypes.TIMESTAMP(3)));
         dataFields.add(new DataField(index++, "date_type", DataTypes.DATE()));
         dataFields.add(new DataField(index++, "decimal_type", DataTypes.DECIMAL(10, 3)));
+        dataFields.add(
+                new DataField(
+                        index++,
+                        "local_timestamp_type",
+                        DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(3)));
 
         RowType rowType = new RowType(dataFields);
         fileFormat.validateDataFields(rowType);
@@ -81,15 +85,4 @@ public class AvroFileFormatTest {
         RowType rowType = new RowType(dataFields);
         fileFormat.validateDataFields(rowType);
     }
-
-    @Test
-    public void testUnsupportedDataTypes() {
-        ArrayList<DataField> dataFields = new ArrayList<>();
-        int index = 0;
-        dataFields.add(new DataField(index++, "timestamp_type", DataTypes.TIMESTAMP(6)));
-
-        RowType rowType = new RowType(dataFields);
-        Assertions.assertThrows(
-                IllegalArgumentException.class, () -> fileFormat.validateDataFields(rowType));
-    }
 }
diff --git a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroToRowDataConvertersTest.java b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroToRowDataConvertersTest.java
new file mode 100644
index 000000000..31cac15a3
--- /dev/null
+++ b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroToRowDataConvertersTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.paimon.format.avro;
+
+import org.apache.paimon.data.Timestamp;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+
+/** Test for avro to row data converters. */
+public class AvroToRowDataConvertersTest {
+    private static final Timestamp NOW = Timestamp.now();
+    private static final long TS_MILLIS = NOW.getMillisecond();
+    private static final long TS_MICROS = NOW.toMicros() + 123L;
+    private static final Timestamp NOW_MICROS = Timestamp.fromMicros(TS_MICROS);
+    private static final Instant INSTANT = Instant.ofEpochMilli(TS_MILLIS);
+
+    @Test
+    public void testConvertToTimestamp() {
+        Assertions.assertEquals(NOW, AvroToRowDataConverters.convertToTimestamp(TS_MILLIS, 3));
+
+        Assertions.assertEquals(
+                NOW_MICROS, AvroToRowDataConverters.convertToTimestamp(TS_MICROS, 6));
+
+        Assertions.assertEquals(NOW, AvroToRowDataConverters.convertToTimestamp(INSTANT, 3));
+
+        Assertions.assertEquals(NOW, AvroToRowDataConverters.convertToTimestamp(INSTANT, 6));
+    }
+}
diff --git a/paimon-format/src/test/java/org/apache/paimon/format/avro/RowDataToAvroConvertersTest.java b/paimon-format/src/test/java/org/apache/paimon/format/avro/RowDataToAvroConvertersTest.java
new file mode 100644
index 000000000..9b38176aa
--- /dev/null
+++ b/paimon-format/src/test/java/org/apache/paimon/format/avro/RowDataToAvroConvertersTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.paimon.format.avro;
+
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.LocalZonedTimestampType;
+
+import org.apache.avro.Schema;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+
+/** Test for row data to avro converters. */
+public class RowDataToAvroConvertersTest {
+
+    @Test
+    public void testTimestampWithTimeType() {
+        LocalZonedTimestampType zonedTimestampType = DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(3);
+        RowDataToAvroConverters.RowDataToAvroConverter rowDataToAvroConverter =
+                RowDataToAvroConverters.createConverter(zonedTimestampType);
+
+        OffsetDateTime offsetDateTime =
+                OffsetDateTime.of(LocalDate.of(2023, 1, 1), LocalTime.of(0, 0, 0), ZoneOffset.UTC);
+
+        Schema schema = AvroSchemaConverter.convertToSchema(zonedTimestampType);
+        long timestamp = offsetDateTime.toInstant().toEpochMilli();
+        Timestamp millis = Timestamp.fromEpochMillis(timestamp);
+        Object converted = rowDataToAvroConverter.convert(schema, millis);
+
+        AvroToRowDataConverters.AvroToRowDataConverter avroToRowDataConverter =
+                AvroToRowDataConverters.createConverter(zonedTimestampType);
+
+        Object original = avroToRowDataConverter.convert(converted);
+        Assertions.assertEquals(millis, original);
+    }
+}
diff --git a/paimon-hive/paimon-hive-common/src/main/java/org/apache/paimon/hive/HiveTypeUtils.java b/paimon-hive/paimon-hive-common/src/main/java/org/apache/paimon/hive/HiveTypeUtils.java
index b2cdf6e75..da2bffc0d 100644
--- a/paimon-hive/paimon-hive-common/src/main/java/org/apache/paimon/hive/HiveTypeUtils.java
+++ b/paimon-hive/paimon-hive-common/src/main/java/org/apache/paimon/hive/HiveTypeUtils.java
@@ -89,6 +89,7 @@ public class HiveTypeUtils {
             case DATE:
                 return TypeInfoFactory.dateTypeInfo;
             case TIMESTAMP_WITHOUT_TIME_ZONE:
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
                 return TypeInfoFactory.timestampTypeInfo;
             case ARRAY:
                 ArrayType arrayType = (ArrayType) logicalType;
diff --git a/paimon-hive/paimon-hive-common/src/test/java/org/apache/paimon/hive/HiveTypeUtilsTest.java b/paimon-hive/paimon-hive-common/src/test/java/org/apache/paimon/hive/HiveTypeUtilsTest.java
index 757b9d17a..ff5b1e9b3 100644
--- a/paimon-hive/paimon-hive-common/src/test/java/org/apache/paimon/hive/HiveTypeUtilsTest.java
+++ b/paimon-hive/paimon-hive-common/src/test/java/org/apache/paimon/hive/HiveTypeUtilsTest.java
@@ -27,7 +27,6 @@ import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 /** Test for {@link HiveTypeUtils}. */
 public class HiveTypeUtilsTest {
@@ -95,12 +94,8 @@ public class HiveTypeUtilsTest {
                                 new DataField(1, "name", new VarCharType(Integer.MAX_VALUE))));
         assertThat(rowTypeInfo.getTypeName()).isEqualTo("struct<id:int,name:string>");
 
-        assertThatExceptionOfType(UnsupportedOperationException.class)
-                .isThrownBy(
-                        () ->
-                                HiveTypeUtils.logicalTypeToTypeInfo(
-                                                DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE())
-                                        .getTypeName())
-                .withMessage("Unsupported logical type TIMESTAMP(6) WITH LOCAL TIME ZONE");
+        TypeInfo timestampWithLocalZoneTypeInfo =
+                HiveTypeUtils.logicalTypeToTypeInfo(DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE());
+        assertThat(timestampWithLocalZoneTypeInfo.getTypeName()).isEqualTo("timestamp");
     }
 }