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

feat: use direct wire format -> LocalDate conversion without resorting to java.util.Date, java.util.Calendar, and default timezones #2464

Merged
merged 1 commit into from
Mar 3, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 26 additions & 20 deletions pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import static org.postgresql.util.internal.Nullness.castNonNull;

import org.postgresql.PGResultSetMetaData;
import org.postgresql.PGStatement;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.BaseStatement;
import org.postgresql.core.Encoding;
Expand Down Expand Up @@ -734,6 +733,30 @@ public int getConcurrency() throws SQLException {
return getTimestampUtils().toLocalDateTime(string);
}

private @Nullable LocalDate getLocalDate(int i) throws SQLException {
byte[] value = getRawValue(i);
if (value == null) {
return null;
}

if (isBinary(i)) {
int col = i - 1;
int oid = fields[col].getOID();
if (oid == Oid.DATE) {
return getTimestampUtils().toLocalDateBin(value);
} else if (oid == Oid.TIMESTAMP) {
return getTimestampUtils().toLocalDateTimeBin(value).toLocalDate();
} else {
davecramer marked this conversation as resolved.
Show resolved Hide resolved
throw new PSQLException(
GT.tr("Cannot convert the column of type {0} to requested type {1}.",
Oid.toString(oid), "java.time.LocalDate"),
PSQLState.DATA_TYPE_MISMATCH);
}
}

return getTimestampUtils().toLocalDateTime(castNonNull(getString(i))).toLocalDate();
davecramer marked this conversation as resolved.
Show resolved Hide resolved
}

public java.sql.@Nullable Date getDate(
String c, java.util.@Nullable Calendar cal) throws SQLException {
return getDate(findColumn(c), cal);
Expand Down Expand Up @@ -3696,25 +3719,8 @@ public void updateArray(String columnName, @Nullable Array x) throws SQLExceptio
}
// JSR-310 support
} else if (type == LocalDate.class) {
if (sqlType == Types.DATE) {
Date dateValue = getDate(columnIndex);
if (dateValue == null) {
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 if (sqlType == Types.TIMESTAMP) {
LocalDateTime localDateTimeValue = getLocalDateTime(columnIndex);
if (localDateTimeValue == null) {
return null;
}
return type.cast(localDateTimeValue.toLocalDate());
if (sqlType == Types.DATE || sqlType == Types.TIMESTAMP) {
return type.cast(getLocalDate(columnIndex));
} else {
throw new PSQLException(GT.tr("conversion to {0} from {1} not supported", type, getPGType(columnIndex)),
PSQLState.INVALID_PARAMETER_VALUE);
Expand Down
31 changes: 28 additions & 3 deletions pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public class TimestampUtils {
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);
private static final Duration PG_EPOCH_DIFF =
Duration.between(Instant.EPOCH, LocalDate.of(2000, 1, 1).atStartOfDay().toInstant(ZoneOffset.UTC));

private static final @Nullable Field DEFAULT_TIME_ZONE_FIELD;

Expand Down Expand Up @@ -1229,7 +1231,7 @@ private ParsedBinaryTimestamp toProlepticParsedTimestampBin(byte[] bytes)
long secs = ts.millis / 1000L;

// postgres epoc to java epoc
secs += 946684800L;
secs += PG_EPOCH_DIFF.getSeconds();
long millis = secs * 1000L;

ts.millis = millis;
Expand Down Expand Up @@ -1258,6 +1260,29 @@ public LocalDateTime toLocalDateTimeBin(byte[] bytes) throws PSQLException {
return LocalDateTime.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos, ZoneOffset.UTC);
}

/**
* Returns the local date time object matching the given bytes with {@link Oid#DATE} or
* {@link Oid#TIMESTAMP}.
* @param bytes The binary encoded local date value.
*
* @return The parsed local date object.
* @throws PSQLException If binary format could not be parsed.
*/
public LocalDate toLocalDateBin(byte[] bytes) throws PSQLException {
if (bytes.length != 4) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "date"),
PSQLState.BAD_DATETIME_FORMAT);
}
int days = ByteConverter.int4(bytes, 0);
if (days == Integer.MAX_VALUE) {
return LocalDate.MAX;
} else if (days == Integer.MIN_VALUE) {
return LocalDate.MIN;
}
// adapt from different Postgres Epoch and convert to LocalDate:
return LocalDate.ofEpochDay(PG_EPOCH_DIFF.toDays() + days);
}

/**
* <p>Given a UTC timestamp {@code millis} finds another point in time that is rendered in given time
* zone {@code tz} exactly as "millis in UTC".</p>
Expand Down Expand Up @@ -1446,7 +1471,7 @@ public String timeToString(java.util.Date time, boolean withTimeZone) {
*/
private static long toJavaSecs(long secs) {
// postgres epoc to java epoc
secs += 946684800L;
secs += PG_EPOCH_DIFF.getSeconds();

// Julian/Gregorian calendar cutoff point
if (secs < -12219292800L) { // October 4, 1582 -> October 15, 1582
Expand All @@ -1470,7 +1495,7 @@ private static long toJavaSecs(long secs) {
*/
private static long toPgSecs(long secs) {
// java epoc to postgres epoc
secs -= 946684800L;
secs -= PG_EPOCH_DIFF.getSeconds();

// Julian/Gregorian calendar cutoff point
if (secs < -13165977600L) { // October 15, 1582 -> October 4, 1582
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,66 @@ public void tearDown() throws SQLException {
*/
@Test
public void testGetLocalDate() throws SQLException {
Statement stmt = con.createStatement();
stmt.executeUpdate(TestUtil.insertSQL("table1","date_column","DATE '1999-01-08'"));
assumeTrue(TestUtil.haveIntegerDateTimes(con));

ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "date_column"));
try {
assertTrue(rs.next());
LocalDate localDate = LocalDate.of(1999, 1, 8);
assertEquals(localDate, rs.getObject("date_column", LocalDate.class));
assertEquals(localDate, rs.getObject(1, LocalDate.class));
} finally {
rs.close();
List<String> zoneIdsToTest = new ArrayList<String>();
zoneIdsToTest.add("Africa/Casablanca"); // It is something like GMT+0..GMT+1
zoneIdsToTest.add("America/Adak"); // It is something like GMT-10..GMT-9
zoneIdsToTest.add("Atlantic/Azores"); // It is something like GMT-1..GMT+0
zoneIdsToTest.add("Europe/Berlin"); // It is something like GMT+1..GMT+2
zoneIdsToTest.add("Europe/Moscow"); // It is something like GMT+3..GMT+4 for 2000s
zoneIdsToTest.add("Pacific/Apia"); // It is something like GMT+13..GMT+14
zoneIdsToTest.add("Pacific/Niue"); // It is something like GMT-11..GMT-11
for (int i = -12; i <= 13; i++) {
zoneIdsToTest.add(String.format("GMT%+02d", i));
}

List<String> datesToTest = Arrays.asList("1998-01-08",
// Some random dates
"1981-12-11", "2022-02-22",
"2015-09-03", "2015-06-30",
"1997-06-30", "1997-07-01", "2012-06-30", "2012-07-01",
"2015-06-30", "2015-07-01", "2005-12-31", "2006-01-01",
"2008-12-31", "2009-01-01", "2015-06-30", "2015-07-31",
"2015-07-31",

// On 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00
"2003-03-25", "2000-03-26", "2000-03-27",

// This is a pre-1970 date, so check if it is rounded properly
"1950-07-20",

// Ensure the calendar is proleptic
"1582-01-01", "1582-12-31",
"1582-09-30", "1582-10-16",

// https://github.com/pgjdbc/pgjdbc/issues/2221
"0001-01-01",
"1000-01-01", "1000-06-01", "0999-12-31",

// On 2000-10-29 03:00:00 Moscow went to regular time, thus local time became 02:00:00
"2000-10-28", "2000-10-29", "2000-10-30");

for (String zoneId : zoneIdsToTest) {
ZoneId zone = ZoneId.of(zoneId);
for (String date : datesToTest) {
localDate(zone, date);
}
}
}

public void localDate(ZoneId zoneId, String date) throws SQLException {
TimeZone.setDefault(TimeZone.getTimeZone(zoneId));
try (Statement stmt = con.createStatement(); ) {
stmt.executeUpdate(TestUtil.insertSQL("table1","date_column","DATE '" + date + "'"));

try (ResultSet rs = stmt.executeQuery(TestUtil.selectSQL("table1", "date_column")); ) {
assertTrue(rs.next());
LocalDate localDate = LocalDate.parse(date);
assertEquals(localDate, rs.getObject("date_column", LocalDate.class));
assertEquals(localDate, rs.getObject(1, LocalDate.class));
}
stmt.executeUpdate("DELETE FROM table1");
}
}

Expand Down Expand Up @@ -204,6 +253,11 @@ public void testGetLocalDateTime() throws SQLException {
// Ensure the calendar is proleptic
"1582-09-30T00:00:00", "1582-10-16T00:00:00",

// https://github.com/pgjdbc/pgjdbc/issues/2221
"0001-01-01T00:00:00",
"1000-01-01T00:00:00",
"1000-01-01T23:59:59", "1000-06-01T01:00:00", "0999-12-31T23:59:59",

// On 2000-10-29 03:00:00 Moscow went to regular time, thus local time became 02:00:00
"2000-10-29T01:59:59", "2000-10-29T02:00:00", "2000-10-29T02:00:01", "2000-10-29T02:59:59",
"2000-10-29T03:00:00", "2000-10-29T03:00:01", "2000-10-29T03:59:59", "2000-10-29T04:00:00",
Expand Down