Skip to content

Commit

Permalink
Restrict timestamp to varchar coercion for historical dates
Browse files Browse the repository at this point in the history
Hive 2.+ and Hive 3.+ uses `java.sql.Timestamp#toString` for coercing Timestamp
to Varchar types. `java.sql.Timestamp#toString` doesn't capture the historical dates correctly
  • Loading branch information
Praveen2112 committed Jul 21, 2023
1 parent 4ce5f46 commit 6699eff
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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"},
Expand Down

0 comments on commit 6699eff

Please sign in to comment.