Skip to content

Commit

Permalink
feat: use direct wire format -> LocalDate conversion without resortin…
Browse files Browse the repository at this point in the history
…g to java.util.Date, java.util.Calendar, and default timezones

Both text and binary formats for date and timestamp contain all the information
that is needed to instantialte LocalDate. There's no need to roundtrip with Date and timezones.

Converting database results into LocalDate directly reduces the number of moving parts and it makes
the code easier to follow.

fixes #2221
  • Loading branch information
uschindler authored and vlsi committed Mar 3, 2022
1 parent a966396 commit c02aa77
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 33 deletions.
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 {
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();
}

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

0 comments on commit c02aa77

Please sign in to comment.