Skip to content

Commit 3609468

Browse files
fix: make all Calendar instances proleptic Gregorian (#3837) (#3887)
* * fix: make all Calendar instances proleptic Gregorian (#3837) Calendar instances created with Calendar.getInstance() or new GregorianCalendar() are by default a hybrid of the Julian and the Gregorian calendar. Java classes like java.sql.Date and SimpleDateFormat also (sometimes) use Calendar instances and therefore might also be affected. This differs from the java.time classes that use the proleptic Gregorian calendar by default. The postgresql server also uses the proleptic Gregorian calendar and therefore it makes sense to make all Calendar instances in the pgjdbc code proleptic Gregorian too. * * fix: make all Calendar instances proleptic Gregorian (#3837) Make TimestampUtils.createProlepticGregorianCalendar(TimeZone) public and use it in all other places too.
1 parent bdd930b commit 3609468

18 files changed

Lines changed: 379 additions & 124 deletions

File tree

benchmarks/src/jmh/java/org/postgresql/benchmark/statement/BindTimestamp.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package org.postgresql.benchmark.statement;
77

8+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
9+
810
import org.postgresql.benchmark.profilers.FlightRecorderProfiler;
911
import org.postgresql.test.TestUtil;
1012

@@ -45,7 +47,7 @@ public class BindTimestamp {
4547
private Connection connection;
4648
private PreparedStatement ps;
4749
private Timestamp ts = new Timestamp(System.currentTimeMillis());
48-
private Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
50+
private Calendar cal = createProlepticGregorianCalendar(TimeZone.getTimeZone("UTC"));
4951

5052
@Setup(Level.Trial)
5153
public void setUp() throws SQLException {
@@ -81,4 +83,5 @@ public static void main(String[] args) throws RunnerException {
8183

8284
new Runner(opt).run();
8385
}
86+
8487
}

benchmarks/src/jmh/java/org/postgresql/benchmark/time/TimestampToDate.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package org.postgresql.benchmark.time;
77

8+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
9+
810
import org.openjdk.jmh.annotations.Benchmark;
911
import org.openjdk.jmh.annotations.BenchmarkMode;
1012
import org.openjdk.jmh.annotations.Fork;
@@ -23,7 +25,6 @@
2325

2426
import java.sql.Timestamp;
2527
import java.util.Calendar;
26-
import java.util.GregorianCalendar;
2728
import java.util.TimeZone;
2829
import java.util.concurrent.TimeUnit;
2930

@@ -41,7 +42,7 @@ public class TimestampToDate {
4142
TimeZone timeZone;
4243

4344
Timestamp ts = new Timestamp(System.currentTimeMillis());
44-
Calendar cachedCalendar = new GregorianCalendar();
45+
Calendar cachedCalendar = createProlepticGregorianCalendar(TimeZone.getDefault());
4546

4647
@Setup
4748
public void init() {

benchmarks/src/jmh/java/org/postgresql/benchmark/time/TimestampToTime.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package org.postgresql.benchmark.time;
77

8+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
9+
810
import org.openjdk.jmh.annotations.Benchmark;
911
import org.openjdk.jmh.annotations.BenchmarkMode;
1012
import org.openjdk.jmh.annotations.Fork;
@@ -41,7 +43,7 @@ public class TimestampToTime {
4143
TimeZone timeZone;
4244

4345
Timestamp ts = new Timestamp(System.currentTimeMillis());
44-
Calendar cachedCalendar = new GregorianCalendar();
46+
Calendar cachedCalendar = createProlepticGregorianCalendar(TimeZone.getDefault());
4547

4648
@Setup
4749
public void init() {

pgjdbc/src/main/java/org/postgresql/PGStatement.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ public interface PGStatement {
2020
// -infinity / infinity representation in Java
2121
long DATE_POSITIVE_INFINITY = 9223372036825200000L;
2222
long DATE_NEGATIVE_INFINITY = -9223372036832400000L;
23-
long DATE_POSITIVE_SMALLER_INFINITY = 185543533774800000L;
24-
long DATE_NEGATIVE_SMALLER_INFINITY = -185543533774800000L;
23+
// Days (2^31) in ms that can be stored minus the difference between the postgres and java epoch
24+
long DATE_POSITIVE_SMALLER_INFINITY = 185541640502400000L;
25+
long DATE_NEGATIVE_SMALLER_INFINITY = -185541640502400000L;
2526

2627
/**
2728
* Returns the Last inserted/updated oid.

pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.postgresql.jdbc;
77

8+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
89
import static org.postgresql.util.internal.Nullness.castNonNull;
910

1011
import org.postgresql.Driver;
@@ -3976,7 +3977,7 @@ public void updateArray(String columnName, @Nullable Array x) throws SQLExceptio
39763977
if (timestampValue == null) {
39773978
return null;
39783979
}
3979-
Calendar calendar = Calendar.getInstance(getDefaultCalendar().getTimeZone());
3980+
Calendar calendar = createProlepticGregorianCalendar(getDefaultCalendar().getTimeZone());
39803981
calendar.setTimeInMillis(timestampValue.getTime());
39813982
return type.cast(calendar);
39823983
} else {

pgjdbc/src/main/java/org/postgresql/jdbc/TimestampUtils.java

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ public class TimestampUtils {
135135
private final StringBuilder sbuf = new StringBuilder();
136136

137137
// This calendar is used when user provides calendar in setX(, Calendar) method.
138-
// It ensures calendar is Gregorian.
139-
private final Calendar calendarWithUserTz = new GregorianCalendar();
138+
// It ensures calendar is proleptic Gregorian.
139+
private final Calendar calendarWithUserTz = createProlepticGregorianCalendar(TimeZone.getDefault());
140140

141141
private @Nullable Calendar calCache;
142142
private @Nullable ZoneOffset calCacheZone;
@@ -159,11 +159,11 @@ private Calendar getCalendar(ZoneOffset offset) {
159159
}
160160

161161
// normally we would use:
162-
// calCache = new GregorianCalendar(TimeZone.getTimeZone(offset));
162+
// calCache = createProlepticGregorianCalendar(TimeZone.getTimeZone(offset));
163163
// But this seems to cause issues for some crazy offsets as returned by server for BC dates!
164164
final String tzid = offset.getTotalSeconds() == 0 ? "UTC" : "GMT".concat(offset.getId());
165165
final TimeZone syntheticTZ = new SimpleTimeZone(offset.getTotalSeconds() * 1000, tzid);
166-
calCache = new GregorianCalendar(syntheticTZ);
166+
calCache = createProlepticGregorianCalendar(syntheticTZ);
167167
calCacheZone = offset;
168168
return calCache;
169169
}
@@ -766,7 +766,7 @@ public OffsetDateTime toOffsetDateTimeBin(byte[] bytes) throws PSQLException {
766766
return new Date(PGStatement.DATE_NEGATIVE_INFINITY);
767767
}
768768
if ( cal == null ) {
769-
cal = Calendar.getInstance();
769+
cal = createProlepticGregorianCalendar(TimeZone.getDefault());
770770
}
771771

772772
ParsedTimestamp pt;
@@ -1782,28 +1782,17 @@ public String timeToString(java.util.Date time, boolean withTimeZone) {
17821782
}
17831783

17841784
/**
1785-
* Converts the given postgresql seconds to java seconds. Reverse engineered by inserting varying
1786-
* dates to postgresql and tuning the formula until the java dates matched. See {@link #toPgSecs}
1785+
* Converts the given postgresql seconds to java seconds. See {@link #toPgSecs}
17871786
* for the reverse operation.
17881787
*
17891788
* @param secs Postgresql seconds.
17901789
* @return Java seconds.
17911790
*/
17921791
@SuppressWarnings("JavaDurationGetSecondsToToSeconds")
17931792
private static long toJavaSecs(long secs) {
1794-
// postgres epoc to java epoc
1793+
// postgres epoch to java epoch
17951794
secs += PG_EPOCH_DIFF.getSeconds();
17961795

1797-
// Julian/Gregorian calendar cutoff point
1798-
if (secs < -12219292800L) { // October 4, 1582 -> October 15, 1582
1799-
secs += 86400 * 10;
1800-
if (secs < -14825808000L) { // 1500-02-28 -> 1500-03-01
1801-
int extraLeaps = (int) ((secs + 14825808000L) / 3155760000L);
1802-
extraLeaps--;
1803-
extraLeaps -= extraLeaps / 4;
1804-
secs += extraLeaps * 86400L;
1805-
}
1806-
}
18071796
return secs;
18081797
}
18091798

@@ -1816,20 +1805,9 @@ private static long toJavaSecs(long secs) {
18161805
*/
18171806
@SuppressWarnings("JavaDurationGetSecondsToToSeconds")
18181807
private static long toPgSecs(long secs) {
1819-
// java epoc to postgres epoc
1808+
// java epoch to postgres epoch
18201809
secs -= PG_EPOCH_DIFF.getSeconds();
18211810

1822-
// Julian/Gregorian calendar cutoff point
1823-
if (secs < -13165977600L) { // October 15, 1582 -> October 4, 1582
1824-
secs -= 86400 * 10;
1825-
if (secs < -15773356800L) { // 1500-03-01 -> 1500-02-28
1826-
int years = (int) ((secs + 15773356800L) / -3155823050L);
1827-
years++;
1828-
years -= years / 4;
1829-
secs += years * 86400L;
1830-
}
1831-
}
1832-
18331811
return secs;
18341812
}
18351813

@@ -1876,6 +1854,23 @@ public static TimeZone parseBackendTimeZone(String timeZone) {
18761854
return TimeZone.getTimeZone(timeZone);
18771855
}
18781856

1857+
/**
1858+
* Create a proleptic Gregorian calendar with the given time zone. This differs from a newly
1859+
* created (Gregorian)Calendar instance that is typically a hybrid of the Julian and Gregorian
1860+
* calendar
1861+
*
1862+
* @param tz the time zone to use
1863+
* @return The proleptic Gregorian Calendar instance
1864+
*/
1865+
@SuppressWarnings("JavaUtilDate") // Using new Date(long) is not problematic on its own
1866+
public static Calendar createProlepticGregorianCalendar(TimeZone tz) {
1867+
GregorianCalendar prolepticGregorianCalendar = new GregorianCalendar(tz);
1868+
// Make the calendar pure (proleptic) Gregorian
1869+
prolepticGregorianCalendar.setGregorianChange(new java.util.Date(Long.MIN_VALUE));
1870+
1871+
return prolepticGregorianCalendar;
1872+
}
1873+
18791874
private static long floorDiv(long x, long y) {
18801875
long r = x / y;
18811876
// if the signs are different and modulo not zero, round down

pgjdbc/src/main/java/org/postgresql/util/PGInterval.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package org.postgresql.util;
77

8+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
9+
810
import org.checkerframework.checker.nullness.qual.Nullable;
911

1012
import java.io.Serializable;
@@ -13,6 +15,7 @@
1315
import java.util.Date;
1416
import java.util.Locale;
1517
import java.util.StringTokenizer;
18+
import java.util.TimeZone;
1619

1720
/**
1821
* This implements a class that handles the PostgreSQL interval type.
@@ -468,7 +471,7 @@ public void add(Date date) {
468471
if (isNull) {
469472
return;
470473
}
471-
final Calendar cal = Calendar.getInstance();
474+
final Calendar cal = createProlepticGregorianCalendar(TimeZone.getDefault());
472475
cal.setTime(date);
473476
add(cal);
474477
date.setTime(cal.getTime().getTime());

pgjdbc/src/test/java/org/postgresql/test/jdbc2/DateTest.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static org.junit.jupiter.api.Assertions.assertNotNull;
1111
import static org.junit.jupiter.api.Assertions.assertTrue;
1212
import static org.junit.jupiter.api.Assumptions.assumeTrue;
13+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
1314

1415
import org.postgresql.test.TestUtil;
1516

@@ -23,6 +24,7 @@
2324
import java.sql.Statement;
2425
import java.util.ArrayList;
2526
import java.util.Arrays;
27+
import java.util.Calendar;
2628
import java.util.List;
2729
import java.util.Locale;
2830
import java.util.Objects;
@@ -318,7 +320,13 @@ private void dateTest() throws SQLException {
318320
st.close();
319321
}
320322

321-
private static java.sql.Date makeDate(int y, int m, int d) {
322-
return new java.sql.Date(y - 1900, m - 1, d);
323+
private static java.sql.Date makeDate(int year, int month, int day) {
324+
Calendar cal = createProlepticGregorianCalendar(TimeZone.getDefault());
325+
cal.clear();
326+
// Note that Calendar.MONTH is zero based
327+
cal.set(year, month - 1, day);
328+
329+
return new java.sql.Date(cal.getTimeInMillis());
323330
}
331+
324332
}

pgjdbc/src/test/java/org/postgresql/test/jdbc2/GetXXXTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.junit.jupiter.api.Assertions.assertEquals;
99
import static org.junit.jupiter.api.Assertions.assertNotNull;
1010
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
1112

1213
import org.postgresql.test.TestUtil;
1314
import org.postgresql.util.PGInterval;
@@ -24,6 +25,7 @@
2425
import java.sql.Timestamp;
2526
import java.util.Calendar;
2627
import java.util.HashMap;
28+
import java.util.TimeZone;
2729

2830
/*
2931
* Test for getObject
@@ -37,7 +39,7 @@ void setUp() throws Exception {
3739
TestUtil.createTempTable(con, "test_interval",
3840
"initial timestamp with time zone, final timestamp with time zone");
3941
PreparedStatement pstmt = con.prepareStatement("insert into test_interval values (?,?)");
40-
Calendar cal = Calendar.getInstance();
42+
Calendar cal = createProlepticGregorianCalendar(TimeZone.getDefault());
4143
cal.add(Calendar.DAY_OF_YEAR, -1);
4244

4345
pstmt.setTimestamp(1, new Timestamp(cal.getTime().getTime()));

pgjdbc/src/test/java/org/postgresql/test/jdbc2/IntervalTest.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.junit.jupiter.api.Assertions.assertFalse;
1010
import static org.junit.jupiter.api.Assertions.assertNotNull;
1111
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
import static org.postgresql.jdbc.TimestampUtils.createProlepticGregorianCalendar;
1213

1314
import org.postgresql.test.TestUtil;
1415
import org.postgresql.util.PGInterval;
@@ -27,8 +28,8 @@
2728
import java.sql.Statement;
2829
import java.util.Calendar;
2930
import java.util.Date;
30-
import java.util.GregorianCalendar;
3131
import java.util.Locale;
32+
import java.util.TimeZone;
3233
import java.util.concurrent.ThreadLocalRandom;
3334

3435
@Isolated("Uses Locale.setDefault")
@@ -148,7 +149,7 @@ void daysHours() throws SQLException {
148149
@Test
149150
void addRounding() {
150151
PGInterval pgi = new PGInterval(0, 0, 0, 0, 0, 0.6006);
151-
Calendar cal = Calendar.getInstance();
152+
Calendar cal = createProlepticGregorianCalendar(TimeZone.getDefault());
152153
long origTime = cal.getTime().getTime();
153154
pgi.add(cal);
154155
long newTime = cal.getTime().getTime();
@@ -208,7 +209,7 @@ void offlineTests() throws Exception {
208209
}
209210

210211
private static Calendar getStartCalendar() {
211-
Calendar cal = new GregorianCalendar();
212+
Calendar cal = createProlepticGregorianCalendar(TimeZone.getDefault());
212213
cal.set(Calendar.YEAR, 2005);
213214
cal.set(Calendar.MONTH, 4);
214215
cal.set(Calendar.DAY_OF_MONTH, 29);
@@ -286,6 +287,25 @@ void date() throws Exception {
286287
assertEquals(date2, date);
287288
}
288289

290+
@Test
291+
void dateYear1000() throws Exception {
292+
final Calendar calYear1000 = createProlepticGregorianCalendar(TimeZone.getDefault());
293+
calYear1000.clear();
294+
calYear1000.set(1000, Calendar.JANUARY, 1);
295+
296+
final Calendar calYear2000 = createProlepticGregorianCalendar(TimeZone.getDefault());
297+
calYear2000.clear();
298+
calYear2000.set(2000, Calendar.JANUARY, 1);
299+
300+
final Date date = calYear1000.getTime();
301+
final Date dateYear2000 = calYear2000.getTime();
302+
303+
PGInterval pgi = new PGInterval("@ +1000 years");
304+
pgi.add(date);
305+
306+
assertEquals(dateYear2000, date);
307+
}
308+
289309
@Test
290310
void postgresDate() throws Exception {
291311
Date date = getStartCalendar().getTime();
@@ -472,7 +492,13 @@ void microSecondsAreRoundedToNearest() throws SQLException {
472492
assertEquals(1, pgi.getMicroSeconds());
473493
}
474494

475-
private static java.sql.Date makeDate(int y, int m, int d) {
476-
return new java.sql.Date(y - 1900, m - 1, d);
495+
private static java.sql.Date makeDate(int year, int month, int day) {
496+
Calendar cal = createProlepticGregorianCalendar(TimeZone.getDefault());
497+
cal.clear();
498+
// Note that Calendar.MONTH is zero based
499+
cal.set(year, month - 1, day);
500+
501+
return new java.sql.Date(cal.getTimeInMillis());
477502
}
503+
478504
}

0 commit comments

Comments
 (0)