Permalink
Browse files

fix: infinity handling for java.time types

Date/Timestamp infinity get converted to LocalDate.MIN/MAX and friends.

fixes #789
  • Loading branch information...
vlsi committed Apr 16, 2017
1 parent 77cace4 commit f375701b571c308b7e389df49c1d958f416e4340
@@ -6,6 +6,7 @@
package org.postgresql.jdbc;
import org.postgresql.PGResultSetMetaData;
import org.postgresql.PGStatement;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.BaseStatement;
import org.postgresql.core.Encoding;
@@ -3308,6 +3309,13 @@ public void updateArray(String columnName, Array x) throws SQLException {
if (wasNull()) {
return null;
}
long time = dateValue.getTime();
if (time == PGStatement.DATE_POSITIVE_INFINITY) {
return type.cast(LocalDate.MAX);
}
if (time == PGStatement.DATE_NEGATIVE_INFINITY) {
return type.cast(LocalDate.MIN);
}
return type.cast(dateValue.toLocalDate());
} else {
throw new SQLException("conversion to " + type + " from " + sqlType + " not supported");
@@ -3330,6 +3338,13 @@ public void updateArray(String columnName, Array x) throws SQLException {
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);
return type.cast(offsetDateTime);
@@ -1046,7 +1046,7 @@ public LocalDateTime toLocalDateTimeBin(TimeZone tz, byte[] bytes) throws PSQLEx
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return LocalDateTime.MAX;
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return LocalDateTime.MAX;
return LocalDateTime.MIN;
}
return LocalDateTime.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos, ZoneOffset.UTC);
@@ -16,11 +16,10 @@
import org.postgresql.jdbc.TimestampUtils;
import org.postgresql.test.TestUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@@ -30,31 +29,46 @@
import java.sql.Timestamp;
import java.sql.Types;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.TimeZone;
/*
* Test get/setTimestamp for both timestamp with time zone and timestamp without time zone datatypes
*
*/
public class TimestampTest {
private Connection con;
@RunWith(Parameterized.class)
public class TimestampTest extends BaseTest4 {
@Before
public TimestampTest(BinaryMode binaryMode) {
setBinaryMode(binaryMode);
}
@Parameterized.Parameters(name = "binary = {0}")
public static Iterable<Object[]> data() {
Collection<Object[]> ids = new ArrayList<Object[]>();
for (BinaryMode binaryMode : BinaryMode.values()) {
ids.add(new Object[]{binaryMode});
}
return ids;
}
@Override
public void setUp() throws Exception {
con = TestUtil.openDB();
super.setUp();
TestUtil.createTable(con, TSWTZ_TABLE, "ts timestamp with time zone");
TestUtil.createTable(con, TSWOTZ_TABLE, "ts timestamp without time zone");
TestUtil.createTable(con, DATE_TABLE, "ts date");
}
@After
public void tearDown() throws Exception {
@Override
public void tearDown() throws SQLException {
TestUtil.dropTable(con, TSWTZ_TABLE);
TestUtil.dropTable(con, TSWOTZ_TABLE);
TestUtil.dropTable(con, DATE_TABLE);
TestUtil.closeDB(con);
super.tearDown();
}
/**
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2017, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.test.jdbc42;
import org.postgresql.core.ServerVersion;
import org.postgresql.test.TestUtil;
import org.postgresql.test.jdbc2.BaseTest4;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@RunWith(Parameterized.class)
public class GetObject310InfinityTests extends BaseTest4 {
private final String expression;
private final String pgType;
private final Class klass;
private final Object expectedValue;
public GetObject310InfinityTests(BinaryMode binaryMode, String expression,
String pgType, Class klass, Object expectedValue) {
setBinaryMode(binaryMode);
this.expression = expression;
this.pgType = pgType;
this.klass = klass;
this.expectedValue = expectedValue;
}
@Override
public void setUp() throws Exception {
super.setUp();
Assume.assumeTrue("PostgreSQL 8.3 does not support 'infinity' for 'date'",
!"date".equals(pgType) || TestUtil.haveMinimumServerVersion(con, ServerVersion.v8_4));
}
@Parameterized.Parameters(name = "binary = {0}, expr = {1}, pgType = {2}, klass = {3}")
public static Iterable<Object[]> data() throws IllegalAccessException {
Collection<Object[]> ids = new ArrayList<Object[]>();
for (BinaryMode binaryMode : BinaryMode.values()) {
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,
OffsetDateTime.class)) {
if (klass.equals(LocalDate.class) && !pgType.equals("date")) {
continue;
}
if (klass.equals(LocalDateTime.class) && !pgType.startsWith("timestamp")) {
continue;
}
if (klass.equals(OffsetDateTime.class) && !pgType.startsWith("timestamp")) {
continue;
}
if (klass.equals(LocalDateTime.class) && pgType.equals("timestamp with time zone")) {
// org.postgresql.util.PSQLException: Cannot convert the column of type TIMESTAMPTZ to requested type timestamp.
continue;
}
Field field = null;
try {
field = klass.getField(expression.startsWith("-") ? "MIN" : "MAX");
} catch (NoSuchFieldException e) {
throw new IllegalStateException("No min/max field in " + klass, e);
}
Object expected = field.get(null);
ids.add(new Object[]{binaryMode, expression, pgType, klass, expected});
}
}
}
}
return ids;
}
@Test
public void test() throws SQLException {
PreparedStatement stmt = con.prepareStatement("select '" + expression + "'::" + pgType);
ResultSet rs = stmt.executeQuery();
rs.next();
Object res = rs.getObject(1, klass);
Assert.assertEquals(expectedValue, res);
}
}
@@ -17,6 +17,7 @@
PreparedStatementTest.class,
Jdbc42CallableStatementTest.class,
GetObject310BinaryTest.class,
GetObject310InfinityTests.class,
SetObject310Test.class})
public class Jdbc42TestSuite {

2 comments on commit f375701

@davecramer

This comment has been minimized.

Member

davecramer replied Apr 17, 2017

I was going to do this, but Infinity is different than MAX.

Should we document this behaviour ?

@vlsi

This comment has been minimized.

Member

vlsi replied Apr 17, 2017

Well, it might make sense to document the behaviour somehow.

Just in case, here's a javadoc for MAX (note "far future"):

    /**
     * The maximum supported {@code LocalDate}, '+999999999-12-31'.
     * This could be used by an application as a "far future" date.
     */
    public static final LocalDate MAX = LocalDate.of(Year.MAX_VALUE, 12, 31);
Please sign in to comment.