Subject: [pgjdbc/pgjdbc] issue #3930: Revert semantic calendar changes introduced with #3837

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Thu, 05 Feb 2026 15:30:50 +0000

**Describe the issue**

The change of calendar handling done in #3837 results in pseudo data corruption (as in: the returned results are different than the stored values) across JDBC versions before and after 42.7.9.

**Driver Version?** 

42.7.8 vs. 42.7.9

**Java Version?**

Not relevant, but I tested with 21 (21.0.6+7-LTS)

**OS Version?**

Not relevant, but I tested on Windows 11 23H2.

**PostgreSQL Version?**

Not relevant, but I tested with the Docker image `postgres:17-alpine` from Dockerhub, image updated today (2026-02-05).

**To Reproduce**
Steps to reproduce the behaviour:

- Create a database with a table that contains a date (or timestamp) field.
- Insert `1000-01-02` with org.postgresql:postgresql:42.7.9 into this table.
- Query the table with a prior version of the driver
  - ⚠️ see incorrect date, namely `1000-01-07`.

The other way around:

- Create a database with a table that contains a date (or timestamp) field.
- Insert `1000-01-02` with org.postgresql:postgresql:42.7.8 or prior into this table.
- Query the table with 42.7.9 version of the driver
  - ⚠️ see incorrect date, namely `999-12-28`.

The following code can be used as stub:
```
Connection connection = DriverManager.getConnection(...);
PreparedStatement stmt = connection.prepareStatement("insert into testtable(this_is_a_date) values (?)");
stmt.setDate(1, java.sql.Date.valueOf(LocalDate.of(1000, 1, 2)));
stmt.executeUpdate();
```

Please also note that `java.sql.Date.valueOf(LocalDate.of(1000, 1, 2))` is just for convenience. The same happens regardless of used Java-side representation, i.e., all of these yield the same results:
- `java.sql.Date.valueOf(LocalDate.of(1000, 1, 2)`
- `java.sql.Date.valueOf(OffsetDateTime.of(1000, 1, 2, 0, 0, 0, 0, ZoneOffset.UTC).toLocalDate())`
- `new java.sql.Date(-30610137600000L)` (if you use this for persisting data with the driver 42.7.9, you will see `1000-01-02` with prior drivers and `999-12-28` with 42.7.9, which what the original change request wanted to achieve)

**Expected behaviour**
This means "seemingly random" "data corruption" occurs for every stored date and timestamp in PostgreSQL databases for time-instances that do not overlap on Gregorian and Julian calendars (as per #3837), i.e., everything before the middle of October, 1582.

"Seemingly random" "data corruption" should not occur at the JDBC-driver level just because I update a patch version. This could be discussed for a major release, but that would still mean that I have to execute data migration scripts in my database to "correct" the already existing data.

I understand that semantically the prior behaviour is incorrect, but consistency of already existing data should take priority in my opinion. What effectively happened with #3837 is that **the semantics of historical dates and timestamps is changed on the JDBC driver level**. This will cause a lot of issues with databases already containing such historical data.

The current state of affairs is even worse: in many DB-browser products the version 42.7.9 of the driver is not yet rolled out, but if Java clients with updated drivers start to commit data into these databases, then the Java clients are seeing a consistent world for the new data ("corrupted" data for anything that existed formerly), but the DB-browser (DBeaver, JetBrains products, etc.) will see a consistent world for the old data and "corrupted" values for anything inserted recently. On the contrary, if I insert a historical dates manually via the DB-browser product, it will display it consistently, but Java clients will read a different value.

**Logs**
N/A

---

From: "vlsi (@vlsi)" <[email-masked]>
Date: Thu, 05 Feb 2026 16:01:23 +0000

Looks like we should indeed better keep backward compatibility rather than introduce incompatible chainges in patch releases.

```
  The Current Issue

  The fix introduced a semantic breaking change that causes apparent "data corruption" for historical dates (pre-1582):
  ┌────────────────────────────────────────────────┬──────────────────┐
  │                    Scenario                    │      Result      │
  ├────────────────────────────────────────────────┼──────────────────┤
  │ Write 1000-01-02 with 42.7.9, read with 42.7.8 │ Shows 1000-01-07 │
  ├────────────────────────────────────────────────┼──────────────────┤
  │ Write 1000-01-02 with 42.7.8, read with 42.7.9 │ Shows 999-12-28  │
  └────────────────────────────────────────────────┴──────────────────┘

  Possible Resolutions
  ┌──────────────────────────┬───────────────────────────────────────┬──────────────────────────────────────────┐
  │          Option          │                 Pros                  │                   Cons                   │
  ├──────────────────────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
  │ Full revert              │ Restores backward compatibility       │ Keeps the technically incorrect behavior │
  ├──────────────────────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
  │ Major version (43.0.0)   │ Proper semver for breaking change     │ Requires re-release                      │
  ├──────────────────────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
  │ Connection property      │ User choice, both behaviors available │ Added complexity, not clear default      │
  ├──────────────────────────┼───────────────────────────────────────┼──────────────────────────────────────────┤
  │ Default old + opt-in new │ Backward compatible with path forward │ Technical correctness not default        │
  └──────────────────────────┴───────────────────────────────────────┴──────────────────────────────────────────┘
```

I do not see win-win case here :-/

Frankly, I am leaning towards "revert behavior, and add an option that could match driver's behavior with database behavior".

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Thu, 05 Feb 2026 16:07:17 +0000

This ship has sailed when `java.sql.Date` and `java.sql.Timestamp` has been bound into JDBC and packed with too much magic (throwing no shade on the original authors, time is extremely difficult, and even the new Java time API is not perfect, I sincerely think that it's an unsolvable problem). The "proper" scenario would be a new JDBC API which uses `java.time.OffsetDateTime` as default, but that will never happen.

I think there is no proper solution here on the JDBC-driver level. Clients, who care about semantical correctness, must correct the data _after_ querying it from the `ResultSet`.

Note that if the decision is made for a backroll, it would be nice to notify the MSSQL JDBC team, they currently have an open issue for introducing the same change https://github.com/microsoft/mssql-jdbc/issues/2789 but it's not merged yet.

---

From: "davecramer (@davecramer)" <[email-masked]>
Date: Thu, 05 Feb 2026 16:22:15 +0000

```This ship has sailed when java.sql.Date and java.sql.Timestamp has been bound into JDBC and packed with too much magic (throwing no shade on the original authors, time is extremely difficult, and even the new Java time API is not perfect, I sincerely think that it's an unsolvable problem). ``` 
Date and Timestamp were the only options at the time.

So you are fine with reverting it even though you can't write and read the same data with 42.7.8 ?

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Thu, 05 Feb 2026 16:28:01 +0000

> Date and Timestamp were the only options at the time.

Exactly what I meant, sure. I'm just saying that correcting this semantical incorrectness now—what, 20 years down the line?—is not really feasable IMO anymore.

Reverting is fine, along as as 42.7.9 is the only released version that has this behaviour. This would mean that one can skip this and jump from 42.7.8 to 42.7.10 directly and notice no semantic change, everything stays consistent.

People who started to fill their database with 42.7.9 (which is as of this writing 21 days old) and will migrate to the currently hypothetical 42.7.10 with the behaviour reverted will see inconsistency in their data.

But that's my argument: I think that dropping and recreating 3 weeks of data is less hassle than creating a migration script for 15+ years of archive data, which would be the case for me. (I know, I sound selfish, but that's how I found the problem in the first place.)

---

From: "davecramer (@davecramer)" <[email-masked]>
Date: Thu, 05 Feb 2026 16:57:30 +0000

When I tested 42.7.8 you could not insert 0001-01-01 and read it back correctly. 
Now if you had 0001-01-01 in the database it read it back fine.

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Fri, 06 Feb 2026 09:24:05 +0000

`0001-01-01` gets more tricky because of the representation PostgreSQL uses for pre-0-year dates (namely postfixing with ` BC` as in you have to insert the string `0044-03-15 BC` into the table in order to get back with a select the date of Julius Caesar's assasination). Note that for such dates the easiest way with JDBC is to handle them as strings on the Java side (it gets very tricky with `PreparedStatement`s).

I have run some more tests for completeness sake and it's much worse than I originally thought:

<details>
<summary>Detailed test results</summary>

| value | inserting via | reading via | result | expected? |
| :---- | :-----------: | :---------: | -----: | :-------: |
| '1000-01-01' | `psql` | `psql` | 1000-01-01 | ✅ |
| '1000-01-01' | `psql` | driver 42.7.8 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | `psql` | driver 42.7.8 with `resultSet.getDate(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | `psql` | driver 42.7.9 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | `psql` | driver 42.7.9 with `resultSet.getDate(...)` | 0999-12-27 | ⛔️ |
| '1000-01-01' | driver 42.7.8 as string | `psql` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getDate(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0999-12-27 | ⚠️ |
| '1000-01-01' | driver 42.7.9 as string | `psql` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getDate(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0999-12-27 | ⛔ |
| '1000-01-01' | driver 42.7.8 as `sql.Date` | `psql` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.8 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.8 with `resultSet.getDate(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.9 with `resultSet.getString(...)` | 1000-01-01 | ✅ |
| '1000-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.9 with `resultSet.getDate(...)` | 0999-12-27 | ⚠️ |
| '1000-01-01' | driver 42.7.9 as `sql.Date` | `psql` | 1000-01-06 | ⛔ |
| '1000-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.8 with `resultSet.getString(...)` | 1000-01-06 | ⚠️ |
| '1000-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.8 with `resultSet.getDate(...)` | 1000-01-06 | ⚠️ |
| '1000-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.9 with `resultSet.getString(...)` | 1000-01-06 | ⛔ |
| '1000-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.9 with `resultSet.getDate(...)` | 1000-01-01 | ✅ |
| '0001-01-01' | `psql` | `psql` | 0001-01-01 | ✅ |
| '0001-01-01' | `psql` | driver 42.7.8 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | `psql` | driver 42.7.8 with `resultSet.getDate(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | `psql` | driver 42.7.9 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | `psql` | driver 42.7.9 with `resultSet.getDate(...)` | 0001-01-03 | ⛔ |
| '0001-01-01' | driver 42.7.8 as string | `psql` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getDate(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0001-01-03 | ⚠️ |
| '0001-01-01' | driver 42.7.9 as string | `psql` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getDate(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0001-01-03 | ⛔ |
| '0001-01-01' | driver 42.7.8 as `sql.Date` | `psql` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.8 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.8 with `resultSet.getDate(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.9 with `resultSet.getString(...)` | 0001-01-01 | ✅ |
| '0001-01-01' | driver 42.7.8 as `sql.Date` | driver 42.7.9 with `resultSet.getDate(...)` | 0001-01-03 | ⚠️ |
| '0001-01-01' | driver 42.7.9 as `sql.Date` | `psql` | 0001-12-30 BC | ⛔ |
| '0001-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.8 with `resultSet.getString(...)` | 0001-12-30 BC | ⚠️ |
| '0001-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.8 with `resultSet.getDate(...)` | 0111-12-30 | ⚠️ |
| '0001-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.9 with `resultSet.getString(...)` | 0001-12-30 BC | ⛔ |
| '0001-01-01' | driver 42.7.9 as `sql.Date` | driver 42.7.9 with `resultSet.getDate(...)` | 0001-01-01 | ✅ |
| '0044-03-15' | `psql` | `psql` | 0044-03-15 | ✅ |
| '0044-03-15' | `psql` | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | `psql` | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | `psql` | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | `psql` | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⛔ |
| '0044-03-15' | driver 42.7.8 as string | `psql` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⚠️ |
| '0044-03-15' | driver 42.7.9 as string | `psql` | 0044-03-15 | ✅|
| '0044-03-15' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⛔ |
| '0044-03-15' | driver 42.7.8 as `sql.Date` | `psql` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as `sql.Date` | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as `sql.Date` | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as `sql.Date` | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 | ✅ |
| '0044-03-15' | driver 42.7.8 as `sql.Date` | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⚠️ |
| '0044-03-15' | driver 42.7.9 as `sql.Date` | `psql` | 0044-03-13 | ⛔ |
| '0044-03-15' | driver 42.7.9 as `sql.Date` | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-13 | ⚠️ |
| '0044-03-15' | driver 42.7.9 as `sql.Date` | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-13 | ⚠️ |
| '0044-03-15' | driver 42.7.9 as `sql.Date` | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-13 | ⛔ |
| '0044-03-15' | driver 42.7.9 as `sql.Date` | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-15 | ✅ |
| '0044-03-15 BC' | `psql` | `psql` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | `psql` | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | `psql` | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ± |
| '0044-03-15 BC' | `psql` | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | `psql` | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⛔ |
| '0044-03-15 BC' | driver 42.7.8 as string | `psql` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.8 as string | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ± |
| '0044-03-15 BC' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.8 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⚠️ |
| '0044-03-15 BC' | driver 42.7.9 as string | `psql` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.9 as string | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ± |
| '0044-03-15 BC' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.9 as string | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⚠️ |
| '0044-03-15 BC' | driver 42.7.8 as `sql.Date`¹ | `psql` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.8 as `sql.Date`¹ | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.8 as `sql.Date`¹ | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-15 | ± |
| '0044-03-15 BC' | driver 42.7.8 as `sql.Date`¹ | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-15 BC | ✅ |
| '0044-03-15 BC' | driver 42.7.8 as `sql.Date`¹ | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-17 | ⚠️ |
| '0044-03-15 BC' | driver 42.7.9 as `sql.Date`¹ | `psql` | 0044-03-13 BC | ⛔ |
| '0044-03-15 BC' | driver 42.7.9 as `sql.Date`¹ | driver 42.7.8 with `resultSet.getString(...)` | 0044-03-13 BC | ⚠️ |
| '0044-03-15 BC' | driver 42.7.9 as `sql.Date`¹ | driver 42.7.8 with `resultSet.getDate(...)` | 0044-03-13 | ⚠️ |
| '0044-03-15 BC' | driver 42.7.9 as `sql.Date`¹ | driver 42.7.9 with `resultSet.getString(...)` | 0044-03-13 BC | ⛔ |
| '0044-03-15 BC' | driver 42.7.9 as `sql.Date`¹ | driver 42.7.9 with `resultSet.getDate(...)` | 0044-03-15 | ± |

¹ done through `java.sql.Date.valueOf(LocalDate.of(-43, 3, 15))`, because `java.sql.Date.valueOf("-0044-03-15")` does not parse (`java.sql.Date` originally did not support BC dates, see it's Javadoc).

</details>

The rows denoted with ± show a known limitiation of `java.sql.Date`. The epoch delivered by the `java.sql.Date#getTime()` is correct for those cases (i.e., different epoch for BC/AD dates that have the same `#toString()` representation).

The originally reported problem is all the rows denoted with ⚠️ Please also note that 42.7.9 delivers a different epoch for BC dates than 42.7.8.

The now discovered hard-cut bug are the rows denoted with ⛔.

This effectively means that if a client has written such dates into a database with 42.7.9, they get incorrect results in both `psql` and via JDBC with previous driver versions. Not only that but querying the same data and accessing the result set either with `getString(...)` or `getDate(...)` will potentially deliver different values.

Vica versa if a client is writing such values with `psql` they get incorrect results with driver 42.7.9, but got correct results with previous driver versions.

**THUS AS SUMMARY:** the PostgreSQL JDBC driver as of version 42.7.9 has a _different semantics_ for pre-Gregorian dates than `psql` itself, which is a huge red flag, and I would consider it a bug. If e.g., I have a query

```
select * from testtable where this_is_a_date <= '1000-01-01';
```

I will get different results of the same data, depending on whether I am executing this query with `psql`, with JDBC pre 42.7.9 or with JDBC 42.7.9. It even depends on whether I insert/query the dates with driver 42.7.9 as strings (i.e., `'1000-01-01'`) or as `sql.Date` (i.e., `java.sql.Date.valueOf(LocalDate.of(1000, 1, 1))`) in my `PreparedStatement`s!

And that is simply put: wrong.

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Fri, 06 Feb 2026 09:41:59 +0000

Simple example for clarity:

Java code:
```
String insert;
insert = "insert into public.testtable(this_is_a_date) values (?)";
statement = connection.prepareStatement(insert);
statement.setDate(1, java.sql.Date.valueOf("1000-01-01"));
statement.executeUpdate();
insert = "insert into public.testtable(this_is_a_date) values ('1000-01-01')";
statement = connection.prepareStatement(insert);
statement.executeUpdate();

String select = "SELECT * FROM public.testtable";
statement = connection.prepareStatement(select);
resultSet = statement.executeQuery();

while (resultSet.next()) {
    String str = resultSet.getString(2);
    java.sql.Date date = resultSet.getDate(2);
    System.out.println("As String: " + str);
    System.out.println("As sql.Date: " + date);
    System.out.println("Epoch within sql.Date: " + date.getTime());
}
```

**Results with driver 42.7.8:**
Output:
```
As String: 1000-01-01
As sql.Date: 1000-01-01
Epoch within sql.Date: -30609795600000
As String: 1000-01-01
As sql.Date: 1000-01-01
Epoch within sql.Date: -30609795600000
```

psql:
```
postgres=# select * from public.testtable;
 id | this_is_a_date
----+----------------
 48 | 1000-01-01
 49 | 1000-01-01
(2 rows)
```

**Result with driver 42.7.9:**
Output:
```
As String: 1000-01-06
As sql.Date: 1000-01-01
Epoch within sql.Date: -30609795600000
As String: 1000-01-01
As sql.Date: 0999-12-27
Epoch within sql.Date: -30610227600000
```

psql:
```
postgres=# select * from public.testtable;
 id | this_is_a_date
----+----------------
 42 | 1000-01-06
 43 | 1000-01-01
(2 rows)
```

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Fri, 06 Feb 2026 10:21:46 +0000

Example the other way around:

Java code:
```

**psql execution:**
```
postgres=# insert into public.testtable(this_is_a_date) values ('1000-01-01');
INSERT 0 1
postgres=# select * from public.testtable;
 id | this_is_a_date
----+----------------
 50 | 1000-01-01
(1 row)
```

**Result with driver 42.7.8:**
```
As String: 1000-01-01
As sql.Date: 1000-01-01
Epoch within sql.Date: -30609795600000
```

---

From: "davecramer (@davecramer)" <[email-masked]>
Date: Fri, 06 Feb 2026 11:54:39 +0000

Ok, I did some more testing and not sure what happened the first time I tested this. I'll revert the change and release shortly.

---

From: "m-van-tilburg (@m-van-tilburg)" <[email-masked]>
Date: Mon, 09 Feb 2026 15:02:02 +0000

> Detailed test results
> value	inserting via	reading via	result	expected?
> '1000-01-01'	`psql`	`psql`	1000-01-01	✅
> '1000-01-01'	`psql`	driver 42.7.8 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	`psql`	driver 42.7.8 with `resultSet.getDate(...)`	1000-01-01	✅
> '1000-01-01'	`psql`	driver 42.7.9 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	`psql`	driver 42.7.9 with `resultSet.getDate(...)`	0999-12-27	⛔️
> '1000-01-01'	driver 42.7.8 as string	`psql`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getDate(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getDate(...)`	0999-12-27	⚠️
> '1000-01-01'	driver 42.7.9 as string	`psql`	1000-01-01	✅
> '1000-01-01'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getDate(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getDate(...)`	0999-12-27	⛔
> '1000-01-01'	driver 42.7.8 as `sql.Date`	`psql`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.8 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.8 with `resultSet.getDate(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.9 with `resultSet.getString(...)`	1000-01-01	✅
> '1000-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.9 with `resultSet.getDate(...)`	0999-12-27	⚠️
> '1000-01-01'	driver 42.7.9 as `sql.Date`	`psql`	1000-01-06	⛔
> '1000-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.8 with `resultSet.getString(...)`	1000-01-06	⚠️
> '1000-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.8 with `resultSet.getDate(...)`	1000-01-06	⚠️
> '1000-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.9 with `resultSet.getString(...)`	1000-01-06	⛔
> '1000-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.9 with `resultSet.getDate(...)`	1000-01-01	✅
> '0001-01-01'	`psql`	`psql`	0001-01-01	✅
> '0001-01-01'	`psql`	driver 42.7.8 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	`psql`	driver 42.7.8 with `resultSet.getDate(...)`	0001-01-01	✅
> '0001-01-01'	`psql`	driver 42.7.9 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	`psql`	driver 42.7.9 with `resultSet.getDate(...)`	0001-01-03	⛔
> '0001-01-01'	driver 42.7.8 as string	`psql`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getDate(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getDate(...)`	0001-01-03	⚠️
> '0001-01-01'	driver 42.7.9 as string	`psql`	0001-01-01	✅
> '0001-01-01'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getDate(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getDate(...)`	0001-01-03	⛔
> '0001-01-01'	driver 42.7.8 as `sql.Date`	`psql`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.8 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.8 with `resultSet.getDate(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.9 with `resultSet.getString(...)`	0001-01-01	✅
> '0001-01-01'	driver 42.7.8 as `sql.Date`	driver 42.7.9 with `resultSet.getDate(...)`	0001-01-03	⚠️
> '0001-01-01'	driver 42.7.9 as `sql.Date`	`psql`	0001-12-30 BC	⛔
> '0001-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.8 with `resultSet.getString(...)`	0001-12-30 BC	⚠️
> '0001-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.8 with `resultSet.getDate(...)`	0111-12-30	⚠️
> '0001-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.9 with `resultSet.getString(...)`	0001-12-30 BC	⛔
> '0001-01-01'	driver 42.7.9 as `sql.Date`	driver 42.7.9 with `resultSet.getDate(...)`	0001-01-01	✅
> '0044-03-15'	`psql`	`psql`	0044-03-15	✅
> '0044-03-15'	`psql`	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	`psql`	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	✅
> '0044-03-15'	`psql`	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	`psql`	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⛔
> '0044-03-15'	driver 42.7.8 as string	`psql`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⚠️
> '0044-03-15'	driver 42.7.9 as string	`psql`	0044-03-15	✅
> '0044-03-15'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⛔
> '0044-03-15'	driver 42.7.8 as `sql.Date`	`psql`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as `sql.Date`	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as `sql.Date`	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as `sql.Date`	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15	✅
> '0044-03-15'	driver 42.7.8 as `sql.Date`	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⚠️
> '0044-03-15'	driver 42.7.9 as `sql.Date`	`psql`	0044-03-13	⛔
> '0044-03-15'	driver 42.7.9 as `sql.Date`	driver 42.7.8 with `resultSet.getString(...)`	0044-03-13	⚠️
> '0044-03-15'	driver 42.7.9 as `sql.Date`	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-13	⚠️
> '0044-03-15'	driver 42.7.9 as `sql.Date`	driver 42.7.9 with `resultSet.getString(...)`	0044-03-13	⛔
> '0044-03-15'	driver 42.7.9 as `sql.Date`	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-15	✅
> '0044-03-15 BC'	`psql`	`psql`	0044-03-15 BC	✅
> '0044-03-15 BC'	`psql`	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	`psql`	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	±
> '0044-03-15 BC'	`psql`	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	`psql`	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⛔
> '0044-03-15 BC'	driver 42.7.8 as string	`psql`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.8 as string	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	±
> '0044-03-15 BC'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.8 as string	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⚠️
> '0044-03-15 BC'	driver 42.7.9 as string	`psql`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.9 as string	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	±
> '0044-03-15 BC'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.9 as string	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⚠️
> '0044-03-15 BC'	driver 42.7.8 as `sql.Date`¹	`psql`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.8 as `sql.Date`¹	driver 42.7.8 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.8 as `sql.Date`¹	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-15	±
> '0044-03-15 BC'	driver 42.7.8 as `sql.Date`¹	driver 42.7.9 with `resultSet.getString(...)`	0044-03-15 BC	✅
> '0044-03-15 BC'	driver 42.7.8 as `sql.Date`¹	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-17	⚠️
> '0044-03-15 BC'	driver 42.7.9 as `sql.Date`¹	`psql`	0044-03-13 BC	⛔
> '0044-03-15 BC'	driver 42.7.9 as `sql.Date`¹	driver 42.7.8 with `resultSet.getString(...)`	0044-03-13 BC	⚠️
> '0044-03-15 BC'	driver 42.7.9 as `sql.Date`¹	driver 42.7.8 with `resultSet.getDate(...)`	0044-03-13	⚠️
> '0044-03-15 BC'	driver 42.7.9 as `sql.Date`¹	driver 42.7.9 with `resultSet.getString(...)`	0044-03-13 BC	⛔
> '0044-03-15 BC'	driver 42.7.9 as `sql.Date`¹	driver 42.7.9 with `resultSet.getDate(...)`	0044-03-15	±

I noticed that `resultSet.getObject("my_timestamp", LocalDateTime.class)` and `resultSet.getObject("my_timestamp", OffsetDateTime.class)` are not in your test results, how do they compare to the other methods? Also I wonder how you obtained the string value for `resultSet.getDate(...)`, are you aware that `java.util.Date.toString()` formats its timestamp value using the Julian calendar for dates before the default cutover date?

I can understand that you consider using the proleptic Gregorian calendar (like the SQL standard and `java.time` describe) for the legacy Java date/time classes a breaking change, because the "pgjdbc" driver (accidentally) did not do this from the start. I hope my well intended contribution to "pgjdbc" did not cause too much problems for your historic dates, I'm sorry if it did.

---

From: "m-van-tilburg (@m-van-tilburg)" <[email-masked]>
Date: Mon, 09 Feb 2026 15:29:10 +0000

> And that is simply put: wrong.

Is there any reason why you are not using `preparedStatement.setObject(int, LocalDate)`?
`java.sql.Date.valueOf(LocalDate)` is unfortunately internally using the deprecated `Date(int, int, int)` constructor. This means the `LocalDate` instance (which is always proleptic Gregorian) is actually interpreted using the Julian calendar if the year is < 1582 (look at `java.util.Date#getCalendarSystem(int)` to see what I mean) and there is no way to control the calendar system in this case.

---

From: "m-van-tilburg (@m-van-tilburg)" <[email-masked]>
Date: Mon, 09 Feb 2026 15:43:31 +0000

> Note that if the decision is made for a backroll, it would be nice to notify the MSSQL JDBC team, they currently have an open issue for introducing the same change [microsoft/mssql-jdbc#2789](https://github.com/microsoft/mssql-jdbc/issues/2789) but it's not merged yet.

I am not sure if you are aware but support for `java.time` classes (using `preparedStatement.setObject()` and `resultSet.getObject()`) was added in [JDBC 4.2](https://download.oracle.com/otndocs/jcp/jdbc-4_2-mrel2-spec/index.html) (Java 8)

As a side note, I tested the legacy date/time classes behavior with three JDBC drivers, "pgjdbc", "mssql-jdbc" and "ojdbc" (Oracle). The Oracle JDBC driver does use the proleptic Gregorian calendar in all cases. For our software (that supports all three databases mentioned above) we configured Hibernate to always use the `java.time` classes for JDBC. This makes things simple because everything is always proleptic Gregorian and the legacy date/time classes can be circumvented all together.

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Tue, 10 Feb 2026 12:36:22 +0000

@m-van-tilburg 

I am answering your comments in a summarized manner instead of point-by-point, I hope that's OK with you :)

> I hope my well intended contribution to "pgjdbc" did not cause too much problems for your historic dates, I'm sorry if it did.

I totally see your point and I see that it was well intended.
I see why the semantic inconsistency is "painful" for Julian vs. Gregorian dates when working with the epochs.
However, it breaks already existing systems with such data.
An even bigger problem is that it introduces an inconsistency on the API-level and incompatibility with `psql`, which is the _de facto_ standard of how stored data shall be interpreted.

> Is there any reason why you are not using preparedStatement.setObject(int, LocalDate)?

I am working at a company where we use a lot of microservices.
I cannot prescribe for each of my developers how do they use JDBC.
Some projects are working with pure SQL and `PreparedStatements`, however they see fit, they can access the data.
Other projects are working with jOOQ, Hibernate, or Spring.

My premise is that I want data to be consistent, regardless how it is inserted and queried.

> I am not sure if you are aware but support for java.time classes (using preparedStatement.setObject() and resultSet.getObject()) was added in JDBC 4.2 (Java 8)

Support, yes, but it's not baked in on the lowest levels, that's what I mean with my tongue-in-cheek comment.

> As a side note, I tested the legacy date/time classes behavior with three JDBC drivers, "pgjdbc", "mssql-jdbc" and "ojdbc" (Oracle).

As detailed in this issue, pgjdbc with version 42.7.9 is inconsistent.

Based on your comment now I have tested with mssql-jdbc (13.3.1.jre11-preview against SQL Server 2025, i.e., 17.00.4006) and it is currently consistent across all possible access methods apart from `OffsetDateTime`.
Inserting ODT with `ZoneOffset.UTC` results in '0999-12-27 00:00:00.0000000'.
Funnily `getObject(OffsetDateTime)` throws `Exception in thread "main" com.microsoft.sqlserver.jdbc.SQLServerException: The conversion from datetime2 to DATETIMEOFFSET is unsupported.`, so the support seems to be incomplete in the driver.

I have also tested with oracle-jdbc (ojdbc17:23.26.1.0.0 against AI Database 26ai Free, i.e., 23.26.1.0.0 with nls_calendar=gregorian) and it is currently consistent across all possible access methods apart from `OffsetDateTime`.
Querying ODT results in `Exception in thread "main" java.sql.SQLException: ORA-18716: {0} not in any time zone.DATE`. Inserting works and is stored as expected.

> LocalDateTime / OffsetDateTime

DATE columns cannot be converted into LDT/ODT, you get an exception:

```
Exception in thread "main" org.postgresql.util.PSQLException: Cannot convert the column of type DATE to requested type java.time.[Local|Offset]DateTime.
```

Nevertheless I tested now with a DATE and with a TIMESTAMP column.
I won't provide all the combinations but here is the gist of it:

Everything is consistent with driver version 42.7.8, i.e.,
* I insert '1000-01-01' with `psql`, via `setDate(java.sql.Date)`, via `setObject(LocalDate)`, or directly as string
  * and I get back the same date, regardless whether I query it in `psql`, via `getString`, `getDate`, or `getObject(LocalDate)`
* I insert '1000-01-01 00:00:00' with `psql`, via `setTimestamp(java.sql.Timestamp)`, via `setObject(LocalDateTime)`, via `setObject(OffsetDateTime)`, or directly as string
  * and I get back the same time, regardless whether I query it in `psql`, via `getString`, `getDate`, `getTimestamp`, `getObject(LocalDateTime)`, or `getObject(OffsetDateTime)`
  * there is one discrepancy, namely inserting as `OffsetDateTime` results in `1000-01-01 00:34:08` when using `ZoneOffset.UTC`, but time zones are messy
  * for reference, returned epoch is -30609795600000. (This is the same epoch as the MSSQL or the Oracle driver returns!)

If I query such inserted data with 42.7.9, I get:
* inserted with `psql` returns correctly for `getString`, `getObject(LocalDate)`, `getObject(LocalDateTime), OffsetDateTime` as '1000-01-01', but as `getDate|Timestamp` it returns '999-12-27'.

The other way, if inserts are done with version 42.7.9:
* `psql` and former driver versions return '1000-01-06' but only if the insert has been done via `setDate|Timestamp`, all the others, i.e., as string or `setObject(LD(T)|ODT)` return '1000-01-01'
* driver version 42.7.9 returns '1000-01-01' for data inserted via `psql` if queried as string, or LD/LDT/ODT, but '999-12-27' if queried with `getDate|Timestamp`.
* driver version 42.7.9 returns '1000-01-06' for data inserted via driver 42.7.9 via `setDate|Timestamp` but queried as string, LD/LDT/ODT; returns '1000-01-01' for the same data if queried with `getDate|Timestamp`.
* etc. pp. according to original report

<details>
<summary>Stub code</summary>

```java
var metaData = connection.getMetaData();
System.out.println("Driver Name: " + metaData.getDriverName());
System.out.println("Driver Version: " + metaData.getDriverVersion());
System.out.println("Driver Major Version: " + metaData.getDriverMajorVersion());
System.out.println("Driver Minor Version: " + metaData.getDriverMinorVersion());
System.out.println("Database Product: " + metaData.getDatabaseProductName());
System.out.println("Database Version: " + metaData.getDatabaseProductVersion());
System.out.println("JDBC Version: " + metaData.getJDBCMajorVersion() + "." + metaData.getJDBCMinorVersion());
System.out.println("================================");

boolean doInsert = false;
if (doInsert) {
    String insert;
    insert = "insert into public.testtable(this_is_a_date, this_is_a_time, inserted_via) values (?, ?, ?)";
    statement = connection.prepareStatement(insert);
    statement.setDate(1, java.sql.Date.valueOf("1000-01-01"));
    statement.setTimestamp(2, java.sql.Timestamp.valueOf("1000-01-01 00:00:00"));
    statement.setString(3, "set(Date|Timestamp)/java.sql.(Date|Timestamp)");
    statement.executeUpdate();
    insert = "insert into public.testtable(this_is_a_date, this_is_a_time, inserted_via) values (?, ?, ?)";
    statement = connection.prepareStatement(insert);
    statement.setObject(1, LocalDate.of(1000, 1, 1));
    statement.setObject(2, LocalDateTime.of(1000, 1, 1, 0, 0, 0, 0));
    statement.setString(3, "setObject/LD(T)");
    statement.executeUpdate();
    statement = connection.prepareStatement(insert);
    statement.setObject(1, LocalDate.of(1000, 1, 1));
    statement.setObject(2, OffsetDateTime.of(1000, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC));
    statement.setString(3, "setObject/ODT");
    statement.executeUpdate();
    insert = "insert into public.testtable(this_is_a_date, this_is_a_time, inserted_via) values ('1000-01-01', '1000-01-01T00:00:00Z', 'as string')";
    statement = connection.prepareStatement(insert);
    statement.executeUpdate();
}

while (resultSet.next()) {
    String via = resultSet.getString(4);
    String str = resultSet.getString(2);
    java.sql.Date date = resultSet.getDate(2);
    LocalDate ld = resultSet.getObject(2, LocalDate.class);
    java.sql.Timestamp ts = resultSet.getTimestamp(3);
    LocalDateTime ldt = resultSet.getObject(3, LocalDateTime.class);
    OffsetDateTime odt = resultSet.getObject(3, OffsetDateTime.class);
    System.out.println("Inserted via: " + via);
    System.out.println("As String: " + str);
    System.out.println("As sql.Date: " + date);
    System.out.println("Epoch within sql.Date: " + date.getTime());
    System.out.println("As sql.Timestamp: " + ts);
    System.out.println("Epoch within sql.Timestamp: " + ts.getTime());
    System.out.println("As LD:" + ld);
    System.out.println("As LDT:" + ldt);
    System.out.println("As ODT:" + odt);
}
```

</details>

---

From: "m-van-tilburg (@m-van-tilburg)" <[email-masked]>
Date: Tue, 10 Feb 2026 15:15:40 +0000

> [@m-van-tilburg](https://github.com/m-van-tilburg)
> 
> I totally see your point and I see that it was well intended. I see why the semantic inconsistency is "painful" for Julian vs. Gregorian dates when working with the epochs. However, it breaks already existing systems with such data. An even bigger problem is that it introduces an inconsistency on the API-level and incompatibility with `psql`, which is the _de facto_ standard of how stored data shall be interpreted.
> 
> I am working at a company where we use a lot of microservices. I cannot prescribe for each of my developers how do they use JDBC. Some projects are working with pure SQL and `PreparedStatements`, however they see fit, they can access the data. Other projects are working with jOOQ, Hibernate, or Spring.
> 
> Based on your comment now I have tested with mssql-jdbc (13.3.1.jre11-preview against SQL Server 2025, i.e., 17.00.4006) and it is currently consistent across all possible access methods apart from `OffsetDateTime`. Inserting ODT with `ZoneOffset.UTC` results in '0999-12-27 00:00:00.0000000'. Funnily `getObject(OffsetDateTime)` throws `Exception in thread "main" com.microsoft.sqlserver.jdbc.SQLServerException: The conversion from datetime2 to DATETIMEOFFSET is unsupported.`, so the support seems to be incomplete in the driver.
> 
> I have also tested with oracle-jdbc (ojdbc17:23.26.1.0.0 against AI Database 26ai Free, i.e., 23.26.1.0.0 with nls_calendar=gregorian) and it is currently consistent across all possible access methods apart from `OffsetDateTime`. Querying ODT results in `Exception in thread "main" java.sql.SQLException: ORA-18716: {0} not in any time zone.DATE`. Inserting works and is stored as expected.
> 
> Nevertheless I tested now with a DATE and with a TIMESTAMP column. I won't provide all the combinations but here is the gist of it:
> 
> Stub code

It might be the case that I missed things and not everything is consistent in 42.7.9 as it should be. I can't verify your mentioned timestamps because I don't know the calendar system and time zone used when you created your Date instance. I believe the difference for your Date/Timestamp is caused by how you interpret/parse the String "1000-01-01" to a Date/Timestamp and the other way around. As I tried to demonstrate in the example code in #3837, this is very easy to get wrong with the legacy date/time classes because they are not explicit about the calendar system (also mixing them up by default) and time zone as `java.time` is. Below are three options to create a Date (unique point in time/instant) from the String "1000-01-01" and format it in different ways, which option resembles the way you created your Date?

```
  @Test
  void dateFromToString() throws ParseException {
    // Uncomment the next line if you like to see the influence of the system default time zone on the parsed timestamps
    //TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));

    // Proleptic Gregorian calendar using the system default time zone
    final Calendar prolepticGregorianCal = new Calendar.Builder().setCalendarType("iso8601").build();
    // Using a hybrid of the Julian and the Gregorian calendar because the default calendar is used!
    final SimpleDateFormat sdfJulianGregorianHybrid = new SimpleDateFormat("yyyy-MM-dd");
    // Using the proleptic Gregorian calendar
    final SimpleDateFormat sdfProlepticGregorian = new SimpleDateFormat("yyyy-MM-dd");
    sdfProlepticGregorian.setCalendar(prolepticGregorianCal);
    // Since the legacy time zone rules can differ from java.time, ZoneId can't be used. use the legacy raw offset instead
    final ZoneOffset calendarZoneOffset = ZoneOffset.ofTotalSeconds(prolepticGregorianCal.getTimeZone().getRawOffset() / 1000);

    /* OPTION 1 to parse the date string */
    final Date fromSdfJulian = sdfJulianGregorianHybrid.parse("1000-01-01");
    // The date is parsed and formatted using the Julian calendar
    assertEquals("1000-01-01", sdfJulianGregorianHybrid.format(fromSdfJulian));
    // The date is parsed using the Julian calendar and formatted using the Gregorian calendar
    assertEquals("1000-01-06", sdfProlepticGregorian.format(fromSdfJulian));
    assertEquals("1000-01-06", fromSdfJulian.toInstant().atOffset(calendarZoneOffset).toLocalDate().toString());

    /* OPTION 2 to parse the date string */
    final Date fromSdfGregorian = sdfProlepticGregorian.parse("1000-01-01");
    // The date is parsed using the Gregorian calendar and formatted using the Julian calendar
    assertEquals("0999-12-27", sdfJulianGregorianHybrid.format(fromSdfGregorian));
    // The date is parsed and formatted using the Gregorian calendar
    assertEquals("1000-01-01", sdfProlepticGregorian.format(fromSdfGregorian));
    assertEquals("1000-01-01", fromSdfGregorian.toInstant().atOffset(calendarZoneOffset).toLocalDate().toString());

    /* OPTION 3 to parse the date string */
    final Date fromLocalDateParseGregorian = Date.from(LocalDate.parse("1000-01-01").atStartOfDay().toInstant(calendarZoneOffset));
    // The date is parsed using the Gregorian calendar and formatted using the Julian calendar
    assertEquals("0999-12-27", sdfJulianGregorianHybrid.format(fromLocalDateParseGregorian));
    // The date is parsed and formatted using the Gregorian calendar
    assertEquals("1000-01-01", sdfProlepticGregorian.format(fromLocalDateParseGregorian));
    assertEquals("1000-01-01", fromLocalDateParseGregorian.toInstant().atOffset(calendarZoneOffset).toLocalDate().toString());

    assertEquals(fromSdfGregorian.getTime(), fromLocalDateParseGregorian.getTime());
    // Uncomment the next line to see that although the date Strings where the same, the parsed timestamps are NOT
    //assertEquals(fromSdfGregorian.getTime(), fromSdfJulian.getTime());

    // Date.toString will format it as Julian using the system default time zone
    System.out.println(fromLocalDateParseGregorian.toString());
  }
```

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Tue, 10 Feb 2026 15:48:07 +0000

I provided the code I am using.

I do not convert any string into a date when executing the query, I am effectively doing
```
resultSet.getDate(...).toString()
resultSet.getString(...)
```

And these must not deliver different results, IMO. Cf. [PoLA](https://en.wikipedia.org/wiki/Principle_of_least_astonishment)

Can we at least agree that if I execute the following with `psql`:
```
insert into public.testtable(this_is_a_date) values ('1000-01-01');
```

Then I can expect to get "1000-01-01" regardless of how am I querying the data? (As is the case for the current versions of the MS-SQL, Oracle drivers and for pgjdbc up till 42.7.8)

My argument is that that `java.sql.Date` and `java.sql.Timestamp` have a semantic issue with pre-Gregorian dates is irrelevant at this point and should not be corrected in the **driver code**. If clients have a problem, they have to correct it after querying it. Otherwise any legacy Java code relying on the current behaviour become effectively broken because of this change. Thus, it can be done, but it **must** be in a major release of the driver, if done at all.

---

From: "m-van-tilburg (@m-van-tilburg)" <[email-masked]>
Date: Tue, 10 Feb 2026 17:30:50 +0000

> My argument is that that `java.sql.Date` and `java.sql.Timestamp` have a semantic issue with pre-Gregorian dates is irrelevant at this point and should not be corrected in the **driver code**. If c…

Now I understand where the difference is coming from. PostgreSQL uses the proleptic Gregorian calendar (see https://www.postgresql.org/docs/17/datetime-units-history.html) and you format your Date instance using the Julian calendar.

- resultSet.getString(...) should return what is actually inside the database, thus always formatted as proleptic Gregorian.
- resultSet.getDate(...) should return a point in time/timestamp that matches with the timestamp that was inserted.
- resultSet.getDate(...).toString() returns what Sun chose 30 years ago in their unfortunate implementation of `java.util.Date` (which is actually a timestamp), thus formatted as Julian for old dates like "1000-01-01". They tried fixing this later by adding `java.util.GregorianCalendar` which allows for more control, but unfortunately also by default is a hybrid of the Julian and Gregorian calendar. Creating Date instances this way thus also might result in timestamps interpreted using the Julian calendar.
- java.sql.Date.valueOf(LocalDate) (which you also used in your examples) was added in Java 8 and may appear to be using proleptic Gregorian to create the internal timestamp value. Unfortunately it uses internal methods that will result in all dates before 1582 to be interpreted using the Julian calendar. What is particularly odd is that the usual month (10) and day (15) do not seem to be considered.

Our software uses date calculations and this mixup of calendars surely returned unexpected results when one date was before 1582-10-15 and the other was after it. We fixed it by using `java.time` practically everywhere so the calendar system and time zone rules are explicit and consistent.

`java.sql.Date` and `java.sql.Timestamp` don't have semantic issues with pre-Gregorian dates, they only store unique points in time (timestamps) just like `java.time.Instant`. It all depends on how you create your timestamps and how you format those timestamps back into local date strings, in this case avoiding Date.toString(). If like you said, everyone in your company is using different methods to parse/interpret, you are bound to get different timestamps. Depending on the calendar system and time zone you use when formatting those timestamps in a particular piece of code, the Strings might look the same, tricking you in believing the timestamps must be the same too while they are not. I agree the change should be in a major release because what you get back from the server is interpreted different from previous versions of the driver. For microsoft/mssql-jdbc#2789 the same consideration might apply.

In 42.7.8 you will get different timestamps depending on how you use the JDBC API. The `java.time` methods will use proleptic Gregorian (like the server). The legacy date/time methods will use a hybrid of Julian and Gregorian because an internal `java.util.Calendar` instance is used that has the default cutover point of 1582-10-15. The fix in 42.7.9 is essentially that the internal `java.util.Calendar` instances are always proleptic Gregorian. This means all clients still using legacy Java date/time classes should interpret/parse/format their timestamps using a proleptic Gregorian `java.util.Calendar` instance, or use `java.time` that is also proleptic Gregorian (different calendars are supported when more advanced Chronology classes are used).

I also used `psql` to insert and select, the results are correct, identical with what I got with the following code using 42.7.9

```
  @Test
  void dateColumn() throws Exception {
    try (Connection dbConnection = DriverManager.getConnection("jdbc:postgresql://YOUR_DB", "USER", "PASSWORD")) {
      dbConnection.createStatement().execute("DELETE FROM public.date_test");

      dbConnection.createStatement().execute("INSERT INTO public.date_test (my_id, my_date) VALUES (1, '1000-01-01')");

      ResultSet resultSet = dbConnection.createStatement().executeQuery("SELECT my_id, my_date FROM public.date_test");
      while (resultSet.next()) {
        assertEquals(LocalDate.of(1000, 1, 1), resultSet.getObject("my_date", LocalDate.class));
        assertEquals("1000-01-01", resultSet.getString("my_date"));
      }
    }
  }
```

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Wed, 11 Feb 2026 07:51:32 +0000

Let me keep it as simple as possible.

**IF** I do `insert into testtable(this_is_a_date) values ('1000-01-01')` (or equivalent for Oracle with `TO_DATE`).

**THEN** I expect the following code to print only `true`, and that 7 times.

```
String select = "SELECT * FROM TESTTABLE";
statement = connection.prepareStatement(select);
resultSet = statement.executeQuery();

while (resultSet.next()) {
    java.sql.Date date = resultSet.getDate("this_is_a_date");
    String string = resultSet.getString("this_is_a_date").split(" ")[0]; // Oracle returns "1000-01-01 00:00:00"
    String[] split = string.split("-");
    LocalDate ld = resultSet.getObject("this_is_a_date", LocalDate.class);

    System.out.println(date.equals(java.sql.Date.valueOf(string)));
    System.out.println(date.equals(java.sql.Date.valueOf(ld)));
    System.out.println(date.toString().equals(string));
    System.out.println(date.toLocalDate().equals(ld));
    System.out.println(date.toLocalDate().toString().equals(string));
    System.out.println(ld.toString().equals(string));
    System.out.println(ld.equals(LocalDate.of(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2]))));
}
```

**RESULTS**

- `com.microsoft.sqlserver:mssql-jdbc:13.3.1.jre11-preview` → 7 × `true`
- `com.oracle.database.jdbc:ojdbc17:23.26.1.0.0` → 7 × `true`
- `org.postgresql:postgresql:42.7.8` → 7 × `true`
- `org.postgresql:postgresql:42.7.9` → 5 × `false` followed by 2 × `true`

---

From: "davecramer (@davecramer)" <[email-masked]>
Date: Wed, 11 Feb 2026 13:19:38 +0000

So the current plan is to release a version with this rollback and then we would entertain the proleptic calendar with a configuration switch.

---

From: "m-van-tilburg (@m-van-tilburg)" <[email-masked]>
Date: Thu, 12 Feb 2026 10:13:17 +0000

> * `com.microsoft.sqlserver:mssql-jdbc:13.3.1.jre11-preview` → 7 × `true`
> * `com.oracle.database.jdbc:ojdbc17:23.26.1.0.0` → 7 × `true`
> * `org.postgresql:postgresql:42.7.8` → 7 × `true`
> * `org.postgresql:postgresql:42.7.9` → 5 × `false` followed by 2 × `true`

I can understand why you would have expected this to be correct before reading the information in this issue. I don't understand it at this point, besides you wanting it to work as it did before. If you don't believe what I am saying, look at the implementation of the `java.util.Date` and `java.sql.Date` methods your calling. Per result this is what happens (keeping in mind that the database is proleptic Gregorian as the SQL standard dictates):

1. "1000-01-01" will be parsed using the Julian calendar (because of `new Date(int, int, int)`) resulting in an incorrect timestamp
2. LocalDate(1000,1,1) will be parsed using the Julian calendar (because of `new Date(int, int, int)`) resulting in an incorrect timestamp
3. The (correct) database timestamp is formatted using the Julian calendar (because of `Date.toString()`) resulting in an incorrect date String
4. `date.toLocalDate()` will use Julian calendar based values as input for the `LocalDate` resulting in an incorrect `LocalDate`
5. same as the previous one except your calling `toString()` on the already incorrect `LocalDate` object
6. correct, because you used `resultset.getObject(String, LocalDate)` to get it so it is (and was in previous versions) correctly returning a proleptic Gregorian value
7. correct, although I am not sure why you test splitting a String against `LocalDate.of(int, int, int)`

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Thu, 12 Feb 2026 10:34:55 +0000

I read the issue description and was aware of the semantic problem in the first place.

What I am saying is that in my opinion it's not the JDBC driver's place to correct a wrong done by Sun in 1996/7. (And as we already established in this thread: dates, times, time zones is an excruciatingly difficult topic to solve, and we don't have a very good solution in any of our programming languages as of yet, but the new Java Time API comes close.)

This change, if rolled out without a feature flag, **will** break already existing and working systems just by upgrading the driver version. As it does with 42.7.9.

---

From: "m-van-tilburg (@m-van-tilburg)" <[email-masked]>
Date: Thu, 12 Feb 2026 10:49:58 +0000

> This change, if rolled out without a feature flag, **will** break already existing and working systems just by upgrading the driver version. As it does with 42.7.9.

I agree with you that date/time can be a difficult topic to understand. I also think this is a breaking change and should be in a major release or behind a feature flag. Luckily there was and is a workaround to get the correct values by using the `java.time` based JDBC methods. Lets close the discussion now.

---

From: "davecramer (@davecramer)" <[email-masked]>
Date: Thu, 12 Feb 2026 10:59:41 +0000

OK, So I'm fine to add the proleptic calendar back in with a feature flag.
Thanks for the this discussion. You two are by far more knowledgeable than I regarding date/time

---

From: "AFulgens (@AFulgens)" <[email-masked]>
Date: Thu, 12 Feb 2026 11:53:02 +0000

@davecramer I thought that's what you meant with "we will enteratain this with a configuration switch" (sorry, I used "feature flag", working with microservices most of the time 😅 ). As long as the default behaviour is rolled back to what it was, all is fine.

@m-van-tilburg 
> Luckily there was and is a workaround to get the correct values by using the java.time based JDBC methods. Lets close the discussion now.

That's sadly not a feasible workaround for a code-base of over 2M lines with 15 years of commit history.

But yes, let's close the discussion and leave the maintainers some breathing room 😀