Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support TIMESTAMP WITH TIME ZONE as ZonedDateTime in JDBC #307

Merged
merged 2 commits into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ abstract class AbstractPrestoResultSet
.add("date", String.class, Date.class, string -> parseDate(string, DateTimeZone.forID(ZoneId.systemDefault().getId())))
.add("time", String.class, Time.class, string -> parseTime(string, ZoneId.systemDefault()))
.add("time with time zone", String.class, Time.class, AbstractPrestoResultSet::parseTimeWithTimeZone)
.add("timestamp", String.class, Timestamp.class, string -> parseTimestamp(string, ZoneId.systemDefault()))
.add("timestamp with time zone", String.class, Timestamp.class, string -> parseTimestamp(string, ZoneId::of))
.add("timestamp", String.class, Timestamp.class, string -> parseTimestampAsSqlTimestamp(string, ZoneId.systemDefault()))
.add("timestamp with time zone", String.class, Timestamp.class, AbstractPrestoResultSet::parseTimestampWithTimeZoneAsSqlTimestamp)
.add("timestamp with time zone", String.class, ZonedDateTime.class, AbstractPrestoResultSet::parseTimestampWithTimeZone)
.add("interval year to month", String.class, PrestoIntervalYearMonth.class, AbstractPrestoResultSet::parseIntervalYearMonth)
.add("interval day to second", String.class, PrestoIntervalDayTime.class, AbstractPrestoResultSet::parseIntervalDayTime)
.add("array", List.class, List.class, (type, list) -> (List<?>) convertFromClientRepresentation(type, list))
Expand Down Expand Up @@ -398,7 +399,7 @@ private Timestamp getTimestamp(int columnIndex, DateTimeZone localTimeZone)
ColumnInfo columnInfo = columnInfo(columnIndex);
if (columnInfo.getColumnTypeSignature().getRawType().equalsIgnoreCase("timestamp")) {
try {
return parseTimestamp((String) value, ZoneId.of(localTimeZone.getID()));
return parseTimestampAsSqlTimestamp((String) value, ZoneId.of(localTimeZone.getID()));
}
catch (IllegalArgumentException e) {
throw new SQLException("Invalid timestamp from server: " + value, e);
Expand All @@ -407,7 +408,7 @@ private Timestamp getTimestamp(int columnIndex, DateTimeZone localTimeZone)

if (columnInfo.getColumnTypeSignature().getRawType().equalsIgnoreCase("timestamp with time zone")) {
try {
return parseTimestamp((String) value, ZoneId::of);
return parseTimestampWithTimeZoneAsSqlTimestamp((String) value);
}
catch (IllegalArgumentException e) {
throw new SQLException("Invalid timestamp from server: " + value, e);
Expand All @@ -417,6 +418,12 @@ private Timestamp getTimestamp(int columnIndex, DateTimeZone localTimeZone)
throw new IllegalArgumentException("Expected column to be a timestamp type but is " + columnInfo.getColumnTypeName());
}

private static ZonedDateTime parseTimestampWithTimeZone(String value)
{
ParsedTimestamp parsed = parseTimestamp(value);
return toZonedDateTime(parsed, timezone -> ZoneId.of(timezone.orElseThrow(() -> new IllegalArgumentException("Time zone missing: " + value))));
}

@Override
public InputStream getAsciiStream(int columnIndex)
throws SQLException
Expand Down Expand Up @@ -1917,18 +1924,27 @@ private static List<ColumnInfo> getColumnInfo(List<Column> columns)
return list.build();
}

private static Timestamp parseTimestamp(String value, ZoneId localTimeZone)
private static Timestamp parseTimestampWithTimeZoneAsSqlTimestamp(String value)
{
ParsedTimestamp parsed = parseTimestamp(value);
return toTimestamp(value, parsed, timezone ->
ZoneId.of(timezone.orElseThrow(() -> new IllegalArgumentException("Time zone missing: " + value))));
}

private static Timestamp parseTimestampAsSqlTimestamp(String value, ZoneId localTimeZone)
{
requireNonNull(localTimeZone, "localTimeZone is null");
return parseTimestamp(value, timezone -> {
if (timezone != null) {

ParsedTimestamp parsed = parseTimestamp(value);
return toTimestamp(value, parsed, timezone -> {
if (timezone.isPresent()) {
throw new IllegalArgumentException("Invalid timestamp: " + value);
}
return localTimeZone;
});
}

private static Timestamp parseTimestamp(String value, Function<String, ZoneId> timeZoneParser)
private static ParsedTimestamp parseTimestamp(String value)
{
Matcher matcher = DATETIME_PATTERN.matcher(value);
if (!matcher.matches()) {
Expand All @@ -1942,15 +1958,31 @@ private static Timestamp parseTimestamp(String value, Function<String, ZoneId> t
int minute = Integer.parseInt(matcher.group("minute"));
int second = Integer.parseInt(matcher.group("second"));
String fraction = matcher.group("fraction");
ZoneId zoneId = timeZoneParser.apply(matcher.group("timezone"));
Optional<String> timezone = Optional.ofNullable(matcher.group("timezone"));

long fractionValue = 0;
long picosOfSecond = 0;
int precision = 0;
if (fraction != null) {
precision = fraction.length();
fractionValue = Long.parseLong(fraction);
verify(precision <= 12, "Unsupported timestamp precision %s: %s", precision, value);
long fractionValue = Long.parseLong(fraction);
picosOfSecond = rescale(fractionValue, precision, 12);
}

return new ParsedTimestamp(year, month, day, hour, minute, second, picosOfSecond, timezone);
}

private static Timestamp toTimestamp(String originalValue, ParsedTimestamp parsed, Function<Optional<String>, ZoneId> timeZoneParser)
{
int year = parsed.year;
int month = parsed.month;
int day = parsed.day;
int hour = parsed.hour;
int minute = parsed.minute;
int second = parsed.second;
long picosOfSecond = parsed.picosOfSecond;
ZoneId zoneId = timeZoneParser.apply(parsed.timezone);

long epochSecond = LocalDateTime.of(year, month, day, hour, minute, second, 0)
.atZone(zoneId)
.toEpochSecond();
Expand All @@ -1959,11 +1991,11 @@ private static Timestamp parseTimestamp(String value, Function<String, ZoneId> t
// slower path, but accurate for historical dates
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day, hour, minute, second);
calendar.setTimeZone(TimeZone.getTimeZone(zoneId));
verify(calendar.getTimeInMillis() % MILLISECONDS_PER_SECOND == 0, "Fractional second when recalculating epochSecond of a historical date: %s", value);
verify(calendar.getTimeInMillis() % MILLISECONDS_PER_SECOND == 0, "Fractional second when recalculating epochSecond of a historical date: %s", originalValue);
epochSecond = calendar.getTimeInMillis() / MILLISECONDS_PER_SECOND;
}

int nanoOfSecond = (int) rescale(fractionValue, precision, 9);
int nanoOfSecond = (int) rescale(picosOfSecond, 12, 9);
if (nanoOfSecond == NANOSECONDS_PER_SECOND) {
epochSecond++;
nanoOfSecond = 0;
Expand All @@ -1974,6 +2006,25 @@ private static Timestamp parseTimestamp(String value, Function<String, ZoneId> t
return timestamp;
}

private static ZonedDateTime toZonedDateTime(ParsedTimestamp parsed, Function<Optional<String>, ZoneId> timeZoneParser)
{
int year = parsed.year;
int month = parsed.month;
int day = parsed.day;
int hour = parsed.hour;
int minute = parsed.minute;
int second = parsed.second;
long picosOfSecond = parsed.picosOfSecond;
ZoneId zoneId = timeZoneParser.apply(parsed.timezone);

ZonedDateTime zonedDateTime = LocalDateTime.of(year, month, day, hour, minute, second, 0)
.atZone(zoneId);

int nanoOfSecond = (int) rescale(picosOfSecond, 12, 9);
zonedDateTime = zonedDateTime.plusNanos(nanoOfSecond);
return zonedDateTime;
}

private static Time parseTime(String value, ZoneId localTimeZone)
{
Matcher matcher = TIME_PATTERN.matcher(value);
Expand Down Expand Up @@ -2089,4 +2140,28 @@ private static boolean isValidOffset(int hour, int minute)
return (hour == 14 && minute == 0) ||
(hour >= 0 && hour < 14 && minute >= 0 && minute <= 59);
}

private static class ParsedTimestamp
{
private final int year;
private final int month;
private final int day;
private final int hour;
private final int minute;
private final int second;
private final long picosOfSecond;
private final Optional<String> timezone;

public ParsedTimestamp(int year, int month, int day, int hour, int minute, int second, long picosOfSecond, Optional<String> timezone)
{
this.year = year;
this.month = month;
this.day = day;
this.hour = hour;
this.minute = minute;
this.second = second;
this.picosOfSecond = picosOfSecond;
this.timezone = requireNonNull(timezone, "timezone is null");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand Down Expand Up @@ -493,10 +494,27 @@ public void testTimestampWithTimeZone()
throws Exception
{
try (ConnectedStatement connectedStatement = newStatement()) {
// zero
checkRepresentation(connectedStatement.getStatement(), "TIMESTAMP '1970-01-01 00:00:00.000 +00:00'", Types.TIMESTAMP_WITH_TIMEZONE, (rs, column) -> {
Timestamp timestampForPointInTime = Timestamp.from(Instant.EPOCH);
assertEquals(rs.getObject(column), timestampForPointInTime);
assertEquals(rs.getObject(column, ZonedDateTime.class), ZonedDateTime.ofInstant(Instant.EPOCH, ZoneId.of("UTC")));
assertThatThrownBy(() -> rs.getDate(column))
.isInstanceOf(SQLException.class)
.hasMessage("Expected value to be a date but is: 1970-01-01 00:00:00.000 UTC");
assertThatThrownBy(() -> rs.getTime(column))
.isInstanceOf(IllegalArgumentException.class) // TODO (https://github.com/prestosql/presto/issues/5315) SQLException
.hasMessage(serverSupportsVariablePrecisionTimestampWithTimeZone()
? "Expected column to be a time type but is timestamp with time zone(3)" // TODO (https://github.com/prestosql/presto/issues/5317) placement of precision parameter
: "Expected column to be a time type but is timestamp with time zone");
assertEquals(rs.getTimestamp(column), timestampForPointInTime);
});

checkRepresentation(connectedStatement.getStatement(), "TIMESTAMP '2018-02-13 13:14:15.227 Europe/Warsaw'", Types.TIMESTAMP_WITH_TIMEZONE, (rs, column) -> {
ZonedDateTime zonedDateTime = ZonedDateTime.of(2018, 2, 13, 13, 14, 15, 227_000_000, ZoneId.of("Europe/Warsaw"));
Timestamp timestampForPointInTime = Timestamp.from(zonedDateTime.toInstant());
assertEquals(rs.getObject(column), timestampForPointInTime); // TODO this should represent TIMESTAMP '2018-02-13 13:14:15.227 Europe/Warsaw'
assertEquals(rs.getObject(column, ZonedDateTime.class), zonedDateTime);
assertThatThrownBy(() -> rs.getDate(column))
.isInstanceOf(SQLException.class)
.hasMessage("Expected value to be a date but is: 2018-02-13 13:14:15.227 Europe/Warsaw");
Expand All @@ -514,6 +532,7 @@ public void testTimestampWithTimeZone()
ZonedDateTime zonedDateTime = ZonedDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneId.of("Europe/Warsaw"));
Timestamp timestampForPointInTime = Timestamp.from(zonedDateTime.toInstant());
assertEquals(rs.getObject(column), timestampForPointInTime); // TODO this should represent TIMESTAMP '2019-12-31 23:59:59.999999999999 Europe/Warsaw'
assertEquals(rs.getObject(column, ZonedDateTime.class), zonedDateTime);
assertThatThrownBy(() -> rs.getDate(column))
.isInstanceOf(SQLException.class)
.hasMessage("Expected value to be a date but is: 2019-12-31 23:59:59.999999999999 Europe/Warsaw");
Expand All @@ -532,6 +551,7 @@ public void testTimestampWithTimeZone()
ZonedDateTime zonedDateTime = ZonedDateTime.of(2020, 1, 1, 0, 0, 0, 0, jvmZone);
Timestamp timestampForPointInTime = Timestamp.from(zonedDateTime.toInstant());
assertEquals(rs.getObject(column), timestampForPointInTime); // TODO this should represent TIMESTAMP '2019-12-31 23:59:59.999999999999 JVM ZONE'
assertEquals(rs.getObject(column, ZonedDateTime.class), zonedDateTime);
assertThatThrownBy(() -> rs.getDate(column))
.isInstanceOf(SQLException.class)
.hasMessage("Expected value to be a date but is: 2019-12-31 23:59:59.999999999999 America/Bahia_Banderas");
Expand Down Expand Up @@ -560,6 +580,7 @@ public void testTimestampWithTimeZone()
ZonedDateTime zonedDateTime = ZonedDateTime.of(1970, 1, 1, 9, 14, 15, 227_000_000, ZoneId.of("Europe/Warsaw"));
Timestamp timestampForPointInTime = Timestamp.from(zonedDateTime.toInstant());
assertEquals(rs.getObject(column), timestampForPointInTime); // TODO this should represent TIMESTAMP '1970-01-01 09:14:15.227 Europe/Warsaw'
assertEquals(rs.getObject(column, ZonedDateTime.class), zonedDateTime);
assertThatThrownBy(() -> rs.getDate(column))
.isInstanceOf(SQLException.class)
.hasMessage("Expected value to be a date but is: 1970-01-01 09:14:15.227 Europe/Warsaw");
Expand All @@ -575,6 +596,7 @@ public void testTimestampWithTimeZone()
ZonedDateTime zonedDateTime = ZonedDateTime.of(1970, 1, 1, 0, 14, 15, 227_000_000, ZoneId.of("Europe/Warsaw"));
Timestamp timestampForPointInTime = Timestamp.from(zonedDateTime.toInstant());
assertEquals(rs.getObject(column), timestampForPointInTime); // TODO this should represent TIMESTAMP '1970-01-01 00:14:15.227 Europe/Warsaw'
assertEquals(rs.getObject(column, ZonedDateTime.class), zonedDateTime);
assertThatThrownBy(() -> rs.getDate(column))
.isInstanceOf(SQLException.class)
.hasMessage("Expected value to be a date but is: 1970-01-01 00:14:15.227 Europe/Warsaw");
Expand All @@ -596,6 +618,7 @@ public void testTimestampWithTimeZone()
ZonedDateTime zonedDateTime = ZonedDateTime.of(12345, 1, 23, 1, 23, 45, 123_456_789, ZoneId.of("Europe/Warsaw"));
Timestamp timestampForPointInTime = Timestamp.from(zonedDateTime.toInstant());
assertEquals(rs.getObject(column), timestampForPointInTime); // TODO this should contain the zone
assertEquals(rs.getObject(column, ZonedDateTime.class), zonedDateTime);
assertThatThrownBy(() -> rs.getDate(column))
.isInstanceOf(SQLException.class)
.hasMessage("Expected value to be a date but is: +12345-01-23 01:23:45.123456789 Europe/Warsaw");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import java.sql.Timestamp;
import java.sql.Types;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.List;
Expand Down Expand Up @@ -299,16 +301,20 @@ public void testTypes()
assertEquals(rs.getTimestamp(5), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis()));
assertEquals(rs.getTimestamp(5, ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis()));
assertEquals(rs.getObject(5), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis()));
assertEquals(rs.getObject(5, ZonedDateTime.class), ZonedDateTime.of(2004, 5, 6, 6, 7, 8, 0, ZoneOffset.ofHoursMinutes(6, 17)));
assertEquals(rs.getTimestamp("e"), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis()));
assertEquals(rs.getTimestamp("e", ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis()));
assertEquals(rs.getObject("e"), new Timestamp(new DateTime(2004, 5, 6, 6, 7, 8, DateTimeZone.forOffsetHoursMinutes(6, 17)).getMillis()));
assertEquals(rs.getObject("e", ZonedDateTime.class), ZonedDateTime.of(2004, 5, 6, 6, 7, 8, 0, ZoneOffset.ofHoursMinutes(6, 17)));

assertEquals(rs.getTimestamp(6), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis()));
assertEquals(rs.getTimestamp(6, ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis()));
assertEquals(rs.getObject(6), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis()));
assertEquals(rs.getObject(6, ZonedDateTime.class), ZonedDateTime.of(2007, 8, 9, 9, 10, 11, 0, ZoneId.of("Europe/Berlin")));
assertEquals(rs.getTimestamp("f"), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis()));
assertEquals(rs.getTimestamp("f", ASIA_ORAL_CALENDAR), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis()));
assertEquals(rs.getObject("f"), new Timestamp(new DateTime(2007, 8, 9, 9, 10, 11, DateTimeZone.forID("Europe/Berlin")).getMillis()));
assertEquals(rs.getObject("f", ZonedDateTime.class), ZonedDateTime.of(2007, 8, 9, 9, 10, 11, 0, ZoneId.of("Europe/Berlin")));

assertEquals(rs.getDate(7), new Date(new DateTime(2013, 3, 22, 0, 0).getMillis()));
assertEquals(rs.getDate(7, ASIA_ORAL_CALENDAR), new Date(new DateTime(2013, 3, 22, 0, 0, ASIA_ORAL_ZONE).getMillis()));
Expand Down