Permalink
Browse files

fix: adjust date, hour, minute, second when rounding timestamp (#1212)

PostgreSQL supports microsecond resolution only, so PgJDBC rounds
nanoseconds to micros. When that happens the number of years, days, hours, seconds, minutes, etc
might change as well

fixes #1211
  • Loading branch information...
vlsi committed Jun 8, 2018
1 parent eaa0aca commit 4dc98be81829bbff3bb00c23214606757df16fab
@@ -20,6 +20,7 @@
import java.sql.Time;
import java.sql.Timestamp;
//#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@@ -48,6 +49,15 @@
private static final char[] ZEROS = {'0', '0', '0', '0', '0', '0', '0', '0', '0'};
private static final char[][] NUMBERS;
private static final HashMap<String, TimeZone> GMT_ZONES = new HashMap<String, TimeZone>();
private static final int MAX_NANOS_BEFORE_WRAP_ON_ROUND = 999999500;
//#if mvn.project.property.postgresql.jdbc.spec >= "JDBC4.2"
private static final Duration ONE_MICROSECOND = Duration.ofNanos(1000);
// LocalTime.MAX is 23:59:59.999_999_999, and it wraps to 24:00:00 when nanos exceed 999_999_499
// since PostgreSQL has microsecond resolution only
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));
//#endif
private static final Field DEFAULT_TIME_ZONE_FIELD;
@@ -538,6 +548,16 @@ public Calendar getSharedCalendar(TimeZone timeZone) {
return tmp;
}
/**
* Returns true when microsecond part of the time should be increased
* when rounding to microseconds
* @param nanos nanosecond part of the time
* @return true when microsecond part of the time should be increased when rounding to microseconds
*/
private static boolean nanosExceed499(int nanos) {
return nanos % 1000 > 499;
}
public synchronized String toString(Calendar cal, Timestamp x) {
return toString(cal, x, true);
}
@@ -551,13 +571,26 @@ public synchronized String toString(Calendar cal, Timestamp x,
}
cal = setupCalendar(cal);
cal.setTime(x);
long timeMillis = x.getTime();
// Round to microseconds
int nanos = x.getNanos();
if (nanos >= MAX_NANOS_BEFORE_WRAP_ON_ROUND) {
nanos = 0;
timeMillis++;
} else if (nanosExceed499(nanos)) {
// PostgreSQL does not support nanosecond resolution yet, and appendTime will just ignore
// 0..999 part of the nanoseconds, however we subtract nanos % 1000 to make the value
// a little bit saner for debugging reasons
nanos += 1000 - nanos % 1000;
}
cal.setTimeInMillis(timeMillis);
sbuf.setLength(0);
appendDate(sbuf, cal);
sbuf.append(' ');
appendTime(sbuf, cal, x.getNanos());
appendTime(sbuf, cal, nanos);
if (withTimeZone) {
appendTimeZone(sbuf, cal);
}
@@ -645,6 +678,16 @@ private static void appendTime(StringBuilder sb, Calendar cal, int nanos) {
appendTime(sb, hours, minutes, seconds, nanos);
}
/**
* Appends time part to the {@code StringBuilder} in PostgreSQL-compatible format.
* The function truncates {@param nanos} to microseconds. The value is expected to be rounded
* beforehand.
* @param sb destination
* @param hours hours
* @param minutes minutes
* @param seconds seconds
* @param nanos nanoseconds
*/
private static void appendTime(StringBuilder sb, int hours, int minutes, int seconds, int nanos) {
sb.append(NUMBERS[hours]);
@@ -654,18 +697,17 @@ private static void appendTime(StringBuilder sb, int hours, int minutes, int sec
sb.append(':');
sb.append(NUMBERS[seconds]);
// Add microseconds, rounded.
// Add nanoseconds.
// This won't work for server versions < 7.2 which only want
// a two digit fractional second, but we don't need to support 7.1
// anymore and getting the version number here is difficult.
//
int microseconds = (nanos / 1000) + (((nanos % 1000) + 500) / 1000);
if (microseconds == 0) {
if (nanos < 1000) {
return;
}
sb.append('.');
int len = sb.length();
sb.append(microseconds);
sb.append(nanos / 1000); // append microseconds
int needZeros = 6 - (sb.length() - len);
if (needZeros > 0) {
sb.insert(len, ZEROS, 0, needZeros);
@@ -733,25 +775,37 @@ public synchronized String toString(LocalTime localTime) {
sbuf.setLength(0);
if (localTime.equals( LocalTime.MAX )) {
if (localTime.isAfter(MAX_TIME)) {
return "24:00:00";
}
int nano = localTime.getNano();
if (nanosExceed499(nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
localTime = localTime.plus(ONE_MICROSECOND);
}
appendTime(sbuf, localTime);
return sbuf.toString();
}
public synchronized String toString(OffsetDateTime offsetDateTime) {
if (OffsetDateTime.MAX.equals(offsetDateTime)) {
if (offsetDateTime.isAfter(MAX_OFFSET_DATETIME)) {
return "infinity";
} else if (OffsetDateTime.MIN.equals(offsetDateTime)) {
return "-infinity";
}
sbuf.setLength(0);
int nano = offsetDateTime.getNano();
if (nanosExceed499(nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
offsetDateTime = offsetDateTime.plus(ONE_MICROSECOND);
}
LocalDateTime localDateTime = offsetDateTime.toLocalDateTime();
LocalDate localDate = localDateTime.toLocalDate();
appendDate(sbuf, localDate);
@@ -768,7 +822,7 @@ public synchronized String toString(OffsetDateTime offsetDateTime) {
* Do not use this method in {@link java.sql.ResultSet#getString(int)}
*/
public synchronized String toString(LocalDateTime localDateTime) {
if (LocalDateTime.MAX.equals(localDateTime)) {
if (localDateTime.isAfter(MAX_LOCAL_DATETIME)) {
return "infinity";
} else if (LocalDateTime.MIN.equals(localDateTime)) {
return "-infinity";
@@ -336,6 +336,8 @@ public void testGetTimestampWOTZ() throws SQLException {
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS8WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS9WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS10WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS1WOTZ_PGFORMAT + "'")));
@@ -355,6 +357,8 @@ public void testGetTimestampWOTZ() throws SQLException {
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS8WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS9WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS10WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS1WOTZ_PGFORMAT + "'")));
@@ -374,6 +378,8 @@ public void testGetTimestampWOTZ() throws SQLException {
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS8WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS9WOTZ_PGFORMAT + "'")));
assertEquals(1,
stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE, "'" + TS10WOTZ_PGFORMAT + "'")));
assertEquals(1, stmt.executeUpdate(TestUtil.insertSQL(TSWOTZ_TABLE,
"'" + tsu.toString(null, new java.sql.Timestamp(tmpDate1WOTZ.getTime())) + "'")));
@@ -412,7 +418,7 @@ public void testGetTimestampWOTZ() throws SQLException {
// Fall through helper
timestampTestWOTZ();
assertEquals(43, stmt.executeUpdate("DELETE FROM " + TSWOTZ_TABLE));
assertEquals(46, stmt.executeUpdate("DELETE FROM " + TSWOTZ_TABLE));
stmt.close();
}
@@ -458,6 +464,9 @@ public void testSetTimestampWOTZ() throws SQLException {
pstmt.setTimestamp(1, TS9WOTZ);
assertEquals(1, pstmt.executeUpdate());
pstmt.setTimestamp(1, TS10WOTZ);
assertEquals(1, pstmt.executeUpdate());
// With java.sql.Timestamp
pstmt.setObject(1, TS1WOTZ, Types.TIMESTAMP);
assertEquals(1, pstmt.executeUpdate());
@@ -477,6 +486,8 @@ public void testSetTimestampWOTZ() throws SQLException {
assertEquals(1, pstmt.executeUpdate());
pstmt.setObject(1, TS9WOTZ, Types.TIMESTAMP);
assertEquals(1, pstmt.executeUpdate());
pstmt.setObject(1, TS10WOTZ, Types.TIMESTAMP);
assertEquals(1, pstmt.executeUpdate());
// With Strings
pstmt.setObject(1, TS1WOTZ_PGFORMAT, Types.TIMESTAMP);
@@ -497,6 +508,8 @@ public void testSetTimestampWOTZ() throws SQLException {
assertEquals(1, pstmt.executeUpdate());
pstmt.setObject(1, TS9WOTZ_PGFORMAT, Types.TIMESTAMP);
assertEquals(1, pstmt.executeUpdate());
pstmt.setObject(1, TS10WOTZ_PGFORMAT, Types.TIMESTAMP);
assertEquals(1, pstmt.executeUpdate());
// With java.sql.Date
pstmt.setObject(1, tmpDate1WOTZ, Types.TIMESTAMP);
@@ -536,7 +549,7 @@ public void testSetTimestampWOTZ() throws SQLException {
// Fall through helper
timestampTestWOTZ();
assertEquals(43, stmt.executeUpdate("DELETE FROM " + TSWOTZ_TABLE));
assertEquals(46, stmt.executeUpdate("DELETE FROM " + TSWOTZ_TABLE));
pstmt.close();
stmt.close();
@@ -715,6 +728,15 @@ private void timestampTestWOTZ() throws SQLException {
tString = rs.getString(1);
assertNotNull(tString);
assertEquals(TS9WOTZ_ROUNDED_PGFORMAT, tString);
assertTrue(rs.next());
t = rs.getTimestamp(1);
assertNotNull(t);
assertEquals(TS10WOTZ_ROUNDED, t);
tString = rs.getString(1);
assertNotNull(tString);
assertEquals(TS10WOTZ_ROUNDED_PGFORMAT, tString);
}
// Testing for Date
@@ -889,6 +911,13 @@ private void timestampTestWOTZ() throws SQLException {
getTimestamp(2000, 2, 7, 15, 0, 0, 1000, null);
private static final String TS9WOTZ_ROUNDED_PGFORMAT = "2000-02-07 15:00:00.000001";
private static final java.sql.Timestamp TS10WOTZ =
getTimestamp(2018, 12, 31, 23, 59, 59, 999999500, null);
private static final String TS10WOTZ_PGFORMAT = "2018-12-31 23:59:59.999999500";
private static final java.sql.Timestamp TS10WOTZ_ROUNDED =
getTimestamp(2019, 1, 1, 0, 0, 0, 0, null);
private static final String TS10WOTZ_ROUNDED_PGFORMAT = "2019-01-01 00:00:00";
private static final String TSWTZ_TABLE = "testtimestampwtz";
private static final String TSWOTZ_TABLE = "testtimestampwotz";
private static final String DATE_TABLE = "testtimestampdate";
@@ -279,6 +279,27 @@ private void offsetTimestamps(ZoneId dataZone, LocalDateTime localDateTime, Stri
}
}
@Test
public void testLocalDateTimeRounding() throws SQLException {
LocalDateTime dateTime = LocalDateTime.parse("2018-12-31T23:59:59.999999500");
localTimestamps(ZoneOffset.UTC, dateTime, "2019-01-01 00:00:00");
}
@Test
public void testTimeStampRounding() throws SQLException {
LocalTime time = LocalTime.parse("23:59:59.999999500");
Time actual = insertThenReadWithoutType(time, "time_without_time_zone_column", Time.class);
assertEquals(Time.valueOf("24:00:00"), actual);
}
@Test
public void testTimeStampRoundingWithType() throws SQLException {
LocalTime time = LocalTime.parse("23:59:59.999999500");
Time actual =
insertThenReadWithType(time, Types.TIME, "time_without_time_zone_column", Time.class);
assertEquals(Time.valueOf("24:00:00"), actual);
}
/**
* Test the behavior of setObject for timestamp columns.
*/

0 comments on commit 4dc98be

Please sign in to comment.