diff --git a/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergConversions.java b/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergConversions.java index b1cc1abf0c81..c5bdaf442011 100644 --- a/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergConversions.java +++ b/paimon-core/src/main/java/org/apache/paimon/iceberg/manifest/IcebergConversions.java @@ -126,6 +126,14 @@ private static ByteBuffer timestampToByteBuffer(Timestamp timestamp, int precisi .putLong(0, timestamp.toMicros()); } + private static Timestamp timestampFromBytes(byte[] bytes, int precision) { + Preconditions.checkArgument( + precision >= 3 && precision <= 6, + "Paimon Iceberg compatibility only support timestamp type with precision from 3 to 6."); + long encoded = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getLong(); + return precision == 3 ? Timestamp.fromEpochMillis(encoded) : Timestamp.fromMicros(encoded); + } + private static ByteBuffer timeToByteBuffer(int millisOfDay, int precision) { Preconditions.checkArgument( precision >= 0 && precision <= 3, @@ -164,17 +172,11 @@ public static Object toPaimonObject(DataType type, byte[] bytes) { return Decimal.fromUnscaledBytes( bytes, decimalType.getPrecision(), decimalType.getScale()); case TIMESTAMP_WITHOUT_TIME_ZONE: + return timestampFromBytes(bytes, ((TimestampType) type).getPrecision()); case TIMESTAMP_WITH_LOCAL_TIME_ZONE: - int timestampPrecision = ((TimestampType) type).getPrecision(); - long timestampLong = - ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getLong(); - Preconditions.checkArgument( - timestampPrecision >= 3 && timestampPrecision <= 6, - "Paimon Iceberg compatibility only support timestamp type with precision from 3 to 6."); - if (timestampPrecision == 3) { - return Timestamp.fromEpochMillis(timestampLong); - } - return Timestamp.fromMicros(timestampLong); + // LocalZonedTimestampType does not extend TimestampType, so it cannot + // share a switch arm with TIMESTAMP_WITHOUT_TIME_ZONE. + return timestampFromBytes(bytes, ((LocalZonedTimestampType) type).getPrecision()); case TIME_WITHOUT_TIME_ZONE: int timePrecision = ((TimeType) type).getPrecision(); Preconditions.checkArgument( diff --git a/paimon-core/src/test/java/org/apache/paimon/iceberg/manifest/IcebergConversionsTimestampTest.java b/paimon-core/src/test/java/org/apache/paimon/iceberg/manifest/IcebergConversionsTimestampTest.java index 677b41d54db8..73da8331c189 100644 --- a/paimon-core/src/test/java/org/apache/paimon/iceberg/manifest/IcebergConversionsTimestampTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/iceberg/manifest/IcebergConversionsTimestampTest.java @@ -125,4 +125,20 @@ void testToPaimonObjectTimestampInvalid(int precision, long serializedMicros) { private static Stream provideInvalidTimestampCases() { return Stream.of(Arguments.of(0, 1698686153L), Arguments.of(9, 1698686153123456789L)); } + + @ParameterizedTest + @MethodSource("provideTimestampToPaimonCases") + @DisplayName("toPaimonObject decodes TIMESTAMP_WITH_LOCAL_TIME_ZONE without ClassCastException") + void testToPaimonObjectForTimestampWithLocalTimeZone( + int precision, long serializedMicros, String expectedTs) { + byte[] bytes = new byte[8]; + ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).putLong(serializedMicros); + + Timestamp actual = + (Timestamp) + IcebergConversions.toPaimonObject( + DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(precision), bytes); + + assertThat(actual.toString()).isEqualTo(expectedTs); + } }