diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveErrorCode.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveErrorCode.java index c628d970b120..2018867e6381 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveErrorCode.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveErrorCode.java @@ -67,6 +67,7 @@ public enum HiveErrorCode HIVE_TABLE_LOCK_NOT_ACQUIRED(40, EXTERNAL), HIVE_VIEW_TRANSLATION_ERROR(41, EXTERNAL), HIVE_PARTITION_NOT_FOUND(42, USER_ERROR), + HIVE_INVALID_TIMESTAMP_COERCION(43, EXTERNAL), /**/; private final ErrorCode errorCode; diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/coercions/TimestampCoercer.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/coercions/TimestampCoercer.java index b44f5295f100..0f9ab56c0dc6 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/coercions/TimestampCoercer.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/coercions/TimestampCoercer.java @@ -14,6 +14,7 @@ package io.trino.plugin.hive.coercions; import io.airlift.slice.Slices; +import io.trino.spi.TrinoException; import io.trino.spi.block.Block; import io.trino.spi.block.BlockBuilder; import io.trino.spi.type.LongTimestamp; @@ -25,9 +26,11 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import static io.trino.plugin.hive.HiveErrorCode.HIVE_INVALID_TIMESTAMP_COERCION; import static io.trino.spi.type.Timestamps.MICROSECONDS_PER_SECOND; import static io.trino.spi.type.Timestamps.NANOSECONDS_PER_MICROSECOND; import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND; +import static io.trino.spi.type.Timestamps.SECONDS_PER_DAY; import static io.trino.spi.type.Varchars.truncateToLength; import static java.lang.Math.floorDiv; import static java.lang.Math.floorMod; @@ -46,6 +49,9 @@ public final class TimestampCoercer .toFormatter() .withChronology(IsoChronology.INSTANCE); + // Before 1900, Java Time and Joda Time are not consistent with java.sql.Date and java.util.Calendar + private static final long START_OF_MODERN_ERA_SECONDS = java.time.LocalDate.of(1900, 1, 1).toEpochDay() * SECONDS_PER_DAY; + private TimestampCoercer() {} public static class LongTimestampToVarcharCoercer @@ -65,6 +71,9 @@ protected void applyCoercedValue(BlockBuilder blockBuilder, Block block, int pos long microsFraction = floorMod(timestamp.getEpochMicros(), MICROSECONDS_PER_SECOND); // Hive timestamp has nanoseconds precision, so no truncation here long nanosFraction = (microsFraction * NANOSECONDS_PER_MICROSECOND) + (timestamp.getPicosOfMicro() / PICOSECONDS_PER_NANOSECOND); + if (epochSecond < START_OF_MODERN_ERA_SECONDS) { + throw new TrinoException(HIVE_INVALID_TIMESTAMP_COERCION, "Coercion on historical dates is not supported"); + } toType.writeSlice( blockBuilder, diff --git a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/coercions/TestTimestampCoercer.java b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/coercions/TestTimestampCoercer.java index deabbe331ee6..2eca2c852d2d 100644 --- a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/coercions/TestTimestampCoercer.java +++ b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/coercions/TestTimestampCoercer.java @@ -15,6 +15,7 @@ import io.airlift.slice.Slices; import io.trino.plugin.hive.HiveTimestampPrecision; +import io.trino.spi.TrinoException; import io.trino.spi.block.Block; import io.trino.spi.type.LongTimestamp; import io.trino.spi.type.SqlTimestamp; @@ -38,6 +39,7 @@ import static java.time.ZoneOffset.UTC; import static java.time.temporal.ChronoField.NANO_OF_SECOND; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class TestTimestampCoercer { @@ -86,11 +88,26 @@ public void testTimestampToSmallerVarchar() assertLongTimestampToVarcharCoercions(TIMESTAMP_PICOS, longTimestamp, createVarcharType(29), "2023-04-11 05:16:12.345678876"); } + @Test + public void testHistoricalLongTimestampToVarchar() + { + LocalDateTime localDateTime = LocalDateTime.parse("1899-12-31T23:59:59.999999999"); + SqlTimestamp timestamp = SqlTimestamp.fromSeconds(TIMESTAMP_PICOS.getPrecision(), localDateTime.toEpochSecond(UTC), localDateTime.get(NANO_OF_SECOND)); + assertThatThrownBy(() -> assertLongTimestampToVarcharCoercions( + TIMESTAMP_PICOS, + new LongTimestamp(timestamp.getEpochMicros(), timestamp.getPicosOfMicros()), + createUnboundedVarcharType(), + "1899-12-31 23:59:59.999999999")) + .isInstanceOf(TrinoException.class) + .hasMessageContaining("Coercion on historical dates is not supported"); + } + @DataProvider public Object[][] timestampValuesProvider() { return new Object[][] { // before epoch + {"1900-01-01T00:00:00.000", "1900-01-01 00:00:00"}, {"1958-01-01T13:18:03.123", "1958-01-01 13:18:03.123"}, // after epoch {"2019-03-18T10:01:17.987", "2019-03-18 10:01:17.987"},