Skip to content
Permalink
Browse files

fix: proleptic java.time support (#1539)

Make the java.time support proleptic by not going through
TimestampUtils.toJavaSecs when in binary mode.

Fixes #1534
  • Loading branch information...
marschall authored and davecramer committed Aug 27, 2019
1 parent 36a75cb commit 60fa6d374a392d00475be0c128804c43b2852a35
@@ -58,7 +58,6 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
//#endif
import java.util.ArrayList;
import java.util.Calendar;
@@ -604,6 +603,44 @@ public Timestamp getTimestamp(int i, java.util.Calendar cal) throws SQLException
}

//#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
private OffsetDateTime getOffsetDateTime(int i) throws SQLException {
checkResultSet(i);
if (wasNullFlag) {
return null;
}

int col = i - 1;
int oid = fields[col].getOID();

if (isBinary(i)) {
if (oid == Oid.TIMESTAMPTZ || oid == Oid.TIMESTAMP) {
return connection.getTimestampUtils().toOffsetDateTimeBin(thisRow[col]);
} else if (oid == Oid.TIMETZ) {
// JDBC spec says timetz must be supported
Time time = getTime(i);
return connection.getTimestampUtils().toOffsetDateTime(time);
} else {
throw new PSQLException(
GT.tr("Cannot convert the column of type {0} to requested type {1}.",
Oid.toString(oid), "timestamptz"),
PSQLState.DATA_TYPE_MISMATCH);
}
}

// If this is actually a timestamptz, the server-provided timezone will override
// the one we pass in, which is the desired behaviour. Otherwise, we'll
// interpret the timezone-less value in the provided timezone.
String string = getString(i);
if (oid == Oid.TIMETZ) {
// JDBC spec says timetz must be supported
// If server sends us a TIMETZ, we ensure java counterpart has date of 1970-01-01
Calendar cal = getDefaultCalendar();
Time time = connection.getTimestampUtils().toTime(cal, string);
return connection.getTimestampUtils().toOffsetDateTime(time);
}
return connection.getTimestampUtils().toOffsetDateTime(string);
}

private LocalDateTime getLocalDateTime(int i) throws SQLException {
checkResultSet(i);
if (wasNullFlag) {
@@ -619,8 +656,7 @@ private LocalDateTime getLocalDateTime(int i) throws SQLException {
PSQLState.DATA_TYPE_MISMATCH);
}
if (isBinary(i)) {
TimeZone timeZone = getDefaultCalendar().getTimeZone();
return connection.getTimestampUtils().toLocalDateTimeBin(timeZone, thisRow[col]);
return connection.getTimestampUtils().toLocalDateTimeBin(thisRow[col]);
}

String string = getString(i);
@@ -3366,19 +3402,7 @@ public void updateArray(String columnName, Array x) throws SQLException {
}
} else if (type == OffsetDateTime.class) {
if (sqlType == Types.TIMESTAMP_WITH_TIMEZONE || sqlType == Types.TIMESTAMP) {
Timestamp timestampValue = getTimestamp(columnIndex);
if (wasNull()) {
return null;
}
long time = timestampValue.getTime();
if (time == PGStatement.DATE_POSITIVE_INFINITY) {
return type.cast(OffsetDateTime.MAX);
}
if (time == PGStatement.DATE_NEGATIVE_INFINITY) {
return type.cast(OffsetDateTime.MIN);
}
// Postgres stores everything in UTC and does not keep original time zone
OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(timestampValue.toInstant(), ZoneOffset.UTC);
OffsetDateTime offsetDateTime = getOffsetDateTime(columnIndex);
return type.cast(offsetDateTime);
} else {
throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
@@ -21,6 +21,7 @@
import java.sql.Timestamp;
//#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@@ -56,6 +57,10 @@
private static final LocalTime MAX_TIME = LocalTime.MAX.minus(Duration.ofMillis(500));
private static final OffsetDateTime MAX_OFFSET_DATETIME = OffsetDateTime.MAX.minus(Duration.ofMillis(500));
private static final LocalDateTime MAX_LOCAL_DATETIME = LocalDateTime.MAX.minus(Duration.ofMillis(500));
// low value for dates is 4713 BC
private static final LocalDate MIN_LOCAL_DATE = LocalDate.of(4713, 1, 1).with(ChronoField.ERA, IsoEra.BCE.getValue());
private static final LocalDateTime MIN_LOCAL_DATETIME = MIN_LOCAL_DATE.atStartOfDay();
private static final OffsetDateTime MIN_OFFSET_DATETIME = MIN_LOCAL_DATETIME.atOffset(ZoneOffset.UTC);
//#endif

private static final Field DEFAULT_TIME_ZONE_FIELD;
@@ -469,6 +474,83 @@ public LocalDateTime toLocalDateTime(String s) throws SQLException {
return result;
}
}

/**
* Parse a string and return a LocalDateTime representing its value.
*
* @param s The ISO formated date string to parse.
* @return null if s is null or a LocalDateTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public OffsetDateTime toOffsetDateTime(String s) throws SQLException {
if (s == null) {
return null;
}

int slen = s.length();

// convert postgres's infinity values to internal infinity magic value
if (slen == 8 && s.equals("infinity")) {
return OffsetDateTime.MAX;
}

if (slen == 9 && s.equals("-infinity")) {
return OffsetDateTime.MIN;
}

ParsedTimestamp ts = parseBackendTimestamp(s);

Calendar tz = ts.tz;
int offsetSeconds;
if (tz == null) {
offsetSeconds = 0;
} else {
offsetSeconds = tz.get(Calendar.ZONE_OFFSET) / 1000;
}
ZoneOffset zoneOffset = ZoneOffset.ofTotalSeconds(offsetSeconds);
// Postgres is always UTC
OffsetDateTime result = OffsetDateTime.of(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.nanos, zoneOffset)
.withOffsetSameInstant(ZoneOffset.UTC);
if (ts.era == GregorianCalendar.BC) {
return result.with(ChronoField.ERA, IsoEra.BCE.getValue());
} else {
return result;
}
}

/**
* Returns the offset date time object matching the given bytes with Oid#TIMETZ.
*
* @param t the time value
* @return the matching offset date time
*/
public OffsetDateTime toOffsetDateTime(Time t) {
// hardcode utc because the backend does not provide us the timezone
// hardoce UNIX epoch, JDBC requires OffsetDateTime but doesn't describe what date should be used
return t.toLocalTime().atDate(LocalDate.of(1970, 1, 1)).atOffset(ZoneOffset.UTC);
}

/**
* Returns the offset date time object matching the given bytes with Oid#TIMESTAMPTZ.
*
* @param bytes The binary encoded local date time value.
* @return The parsed local date time object.
* @throws PSQLException If binary format could not be parsed.
*/
public OffsetDateTime toOffsetDateTimeBin(byte[] bytes) throws PSQLException {
ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return OffsetDateTime.MAX;
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return OffsetDateTime.MIN;
}

// hardcode utc because the backend does not provide us the timezone
// Postgres is always UTC
Instant instant = Instant.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos);
return OffsetDateTime.ofInstant(instant, ZoneOffset.UTC);
}

//#endif

public synchronized Time toTime(Calendar cal, String s) throws SQLException {
@@ -758,7 +840,7 @@ private static void appendEra(StringBuilder sb, Calendar cal) {
public synchronized String toString(LocalDate localDate) {
if (LocalDate.MAX.equals(localDate)) {
return "infinity";
} else if (LocalDate.MIN.equals(localDate)) {
} else if (localDate.isBefore(MIN_LOCAL_DATE)) {
return "-infinity";
}

@@ -792,7 +874,7 @@ public synchronized String toString(LocalTime localTime) {
public synchronized String toString(OffsetDateTime offsetDateTime) {
if (offsetDateTime.isAfter(MAX_OFFSET_DATETIME)) {
return "infinity";
} else if (OffsetDateTime.MIN.equals(offsetDateTime)) {
} else if (offsetDateTime.isBefore(MIN_OFFSET_DATETIME)) {
return "-infinity";
}

@@ -824,7 +906,7 @@ public synchronized String toString(OffsetDateTime offsetDateTime) {
public synchronized String toString(LocalDateTime localDateTime) {
if (localDateTime.isAfter(MAX_LOCAL_DATETIME)) {
return "infinity";
} else if (LocalDateTime.MIN.equals(localDateTime)) {
} else if (localDateTime.isBefore(MIN_LOCAL_DATETIME)) {
return "-infinity";
}

@@ -1056,8 +1138,8 @@ public Timestamp toTimestampBin(TimeZone tz, byte[] bytes, boolean timestamptz)
return ts;
}

private ParsedBinaryTimestamp toParsedTimestampBin(TimeZone tz, byte[] bytes, boolean timestamptz)
throws PSQLException {
private ParsedBinaryTimestamp toParsedTimestampBinPlain(byte[] bytes)
throws PSQLException {

if (bytes.length != 8) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "timestamp"),
@@ -1106,6 +1188,24 @@ private ParsedBinaryTimestamp toParsedTimestampBin(TimeZone tz, byte[] bytes, bo
}
nanos *= 1000;

long millis = secs * 1000L;

ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.millis = millis;
ts.nanos = nanos;
return ts;
}

private ParsedBinaryTimestamp toParsedTimestampBin(TimeZone tz, byte[] bytes, boolean timestamptz)
throws PSQLException {

ParsedBinaryTimestamp ts = toParsedTimestampBinPlain(bytes);
if (ts.infinity != null) {
return ts;
}

long secs = ts.millis / 1000L;

secs = toJavaSecs(secs);
long millis = secs * 1000L;
if (!timestamptz) {
@@ -1114,31 +1214,48 @@ private ParsedBinaryTimestamp toParsedTimestampBin(TimeZone tz, byte[] bytes, bo
millis = guessTimestamp(millis, tz);
}

ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.millis = millis;
ts.nanos = nanos;
return ts;
}

private ParsedBinaryTimestamp toProlepticParsedTimestampBin(byte[] bytes)
throws PSQLException {

ParsedBinaryTimestamp ts = toParsedTimestampBinPlain(bytes);
if (ts.infinity != null) {
return ts;
}

long secs = ts.millis / 1000L;

// postgres epoc to java epoc
secs += 946684800L;
long millis = secs * 1000L;

ts.millis = millis;
return ts;
}

//#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
/**
* Returns the local date time object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
*
* @param tz time zone to use
* @param bytes The binary encoded local date time value.
*
* @return The parsed local date time object.
* @throws PSQLException If binary format could not be parsed.
*/
public LocalDateTime toLocalDateTimeBin(TimeZone tz, byte[] bytes) throws PSQLException {
public LocalDateTime toLocalDateTimeBin(byte[] bytes) throws PSQLException {

ParsedBinaryTimestamp parsedTimestamp = this.toParsedTimestampBin(tz, bytes, true);
ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return LocalDateTime.MAX;
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return LocalDateTime.MIN;
}

// hardcode utc because the backend does not provide us the timezone
// Postgres is always UTC
return LocalDateTime.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos, ZoneOffset.UTC);
}
//#endif
@@ -1426,4 +1543,5 @@ private static long floorDiv(long x, long y) {
private static long floorMod(long x, long y) {
return x - floorDiv(x, y) * y;
}

}
@@ -30,11 +30,11 @@
public class GetObject310InfinityTests extends BaseTest4 {
private final String expression;
private final String pgType;
private final Class klass;
private final Class<?> klass;
private final Object expectedValue;

public GetObject310InfinityTests(BinaryMode binaryMode, String expression,
String pgType, Class klass, Object expectedValue) {
String pgType, Class<?> klass, Object expectedValue) {
setBinaryMode(binaryMode);
this.expression = expression;
this.pgType = pgType;
@@ -56,7 +56,7 @@ public void setUp() throws Exception {
for (String expression : Arrays.asList("-infinity", "infinity")) {
for (String pgType : Arrays.asList("date", "timestamp",
"timestamp with time zone")) {
for (Class klass : Arrays.asList(LocalDate.class, LocalDateTime.class,
for (Class<?> klass : Arrays.asList(LocalDate.class, LocalDateTime.class,
OffsetDateTime.class)) {
if (klass.equals(LocalDate.class) && !pgType.equals("date")) {
continue;

0 comments on commit 60fa6d3

Please sign in to comment.
You can’t perform that action at this time.