[FEATURE] Lossless java.time support in getObject(col, Class) for DATE/TIMESTAMP (BC and pre-1582 values)

Open
#1,779 2 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@cathleeny is already working on this.

Since Nov 11, 2026.

  • #1781 by @cathleeny — open

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Stale
Tech stack
java
Domain
database

Research direction

Start by reviewing the open pull request #1781 and compare its changes with the requested JDBC 4.2 mappings for getObject(int/String, Class). The issue names no source files or tests; its failing examples cover DATE and TIMESTAMP values, including BC and pre-1582 dates. Done means the requested java.time mappings preserve Arrow source values, unsupported mappings raise SQLException, and existing getters keep their behavior.

Written by the indexing model from the issue text.

Description

Is your feature request related to a problem? Please describe.

Databricks SQL stores DATE and TIMESTAMP on the proleptic Gregorian calendar (Spark 3+), and the server sends exact epoch-days and epoch-microseconds over Arrow. The driver converts them to java.sql.Date / java.sql.Timestamp with Date.valueOf(LocalDate) / Timestamp.valueOf(LocalDateTime). Those legacy types use the hybrid Julian/Gregorian calendar and have no BC era, so values before 1582-10-15 cannot be returned exactly. This is the same behaviour as Spark's own toJavaTimestamp/toJavaDate, and we are not asking to change it.

The problem is that the driver currently has no lossless alternative:

  • getObject(col, Instant.class), getObject(col, LocalDateTime.class) and getObject(col, OffsetDateTime.class) return a java.lang.String (the convertSqlTypeToSpecificJavaType fallback). The caller gets a ClassCastException.
  • getObject(col, LocalDate.class) is derived from java.sql.Date.toLocalDate(), so it inherits the same loss.

Observed on 3.4.4 (also 3.1.1) against a SQL warehouse, with JVM and session in UTC and Arrow results (the default). Server values come from UNIX_MICROS / UNIX_DATE:

Value Server Driver
TO_TIMESTAMP('2016 bc', 'yyyy G') -125754422400000000 µs getTimestamp().toInstant() = -2016-12-15T00:00:00Z (−17 days); toString() = 2016-01-01 00:00:00.0 (era lost)
TIMESTAMP '1000-01-01 00:00:00 UTC' -30610224000000000 µs toInstant() = 1000-01-06T00:00:00Z (+5 days)
CAST(... 2016 bc ... AS DATE) -1455491 days getDate() = 2016-01-01 (AD); getObject(col, LocalDate.class) = 2016-01-01
DATE '1582-10-05' -141437 days getDate() = 1582-10-15; getObject(col, LocalDate.class) = 1582-10-15
getObject(ts, Instant/LocalDateTime/OffsetDateTime) n/a returns String "2016-01-01 00:00:00.0"

Applications that need exact historical values (for example, ingesting tables containing BC or pre-1582 dates) can't get them from the driver today.

Describe the solution you'd like

Support the JDBC 4.2 java.time mappings in getObject(int/String, Class), populated straight from the source value (Arrow vector / server string), without going through java.sql types:

Column LocalDate LocalDateTime Instant OffsetDateTime
DATE exact atStartOfDay() error error
TIMESTAMP_NTZ date part exact error (no zone) error (no zone)
TIMESTAMP session-local date session-local fields (java.time zone rules, matching Databricks rendering) exact exact, UTC offset
  • Leave existing getters (getDate, getTimestamp, getString, getObject(int) and the Calendar overloads) unchanged, so there is no behaviour change for existing users.
  • Requests that can't be satisfied (for example, Instant from TIMESTAMP_NTZ, or a temporal class on a non-temporal column) raise a clear SQLException instead of returning a String.
  • Arrow paths are exact. Text result paths are best effort: SEA JSON_ARRAY results already truncate TIMESTAMP to milliseconds and drop the BC era on the server (TO_TIMESTAMP('2016 bc','yyyy G') arrives as 2016-01-01T00:00:00.000Z).

This is what pgjdbc, Trino, mssql-jdbc, MySQL Connector/J and Arrow Flight SQL JDBC do: legacy types stay field-based, and java.time is lossless. It also matches Spark's own lossless route (spark.sql.datetime.java8API.enabled).

I'm happy to contribute a PR. Proposed split:

  1. Read side: the getObject(col, java.time.*) support above.
  2. Write side, separately:
    • setObject(OffsetDateTime), which is currently unsupported.
    • Explicit ISO formatting with signed years in place of toString().
    • Binding LocalDateTime as TIMESTAMP_NTZ.

Describe alternatives or workarounds you've considered

  • Make java.sql.Timestamp instant-preserving (build it from epoch millis rather than Timestamp.valueOf). Rejected:
    • It changes toString()/getString() for every pre-1582 value; 1000-01-01 would print as 0999-12-27. That breaks read → setTimestamp round-trips.
    • It adds minute-level drift for pre-1900 values on non-UTC JVMs (java.util.TimeZone has no local mean time).
    • It still can't represent BC, year 0 or 1582-10-05..14.
    • It diverges from Spark and the other drivers above.
  • Calendar-mode connection property. Same trade-offs, and it adds a mode to the driver.
  • Rewriting queries to return unix_micros(col) / unix_date(col) in the application. This works but changes result schemas, and needs per-query special handling for nulls and TIMESTAMP_NTZ.

Additional context

Failing test (each one reads a value back and checks it is unchanged). Connection and session are in UTC. The comment on each assertion is what 3.4.4 actually returns:

@Test void dateInGregorianGap() throws SQLException {
  ResultSet rs = stmt.executeQuery("SELECT DATE '1582-10-05'"); rs.next();
  assertEquals(LocalDate.of(1582, 10, 5), rs.getObject(1, LocalDate.class));
  // actual: 1582-10-15 (moved 10 days forward)
}

@Test void bcDate() throws SQLException {
  ResultSet rs = stmt.executeQuery("SELECT DATE '-2015-01-01'"); rs.next();  // 2016 BC
  assertEquals(LocalDate.of(-2015, 1, 1), rs.getObject(1, LocalDate.class));
  // actual: 2016-01-01 (BC turned into AD)
}

@Test void timestampBefore1582() throws SQLException {
  ResultSet rs = stmt.executeQuery("SELECT TIMESTAMP '1000-01-01 00:00:00 UTC'"); rs.next();
  assertEquals(Instant.parse("1000-01-01T00:00:00Z"), rs.getTimestamp(1).toInstant());
  // actual: 1000-01-06T00:00:00Z (5 days late)
}

@Test void getObjectInstant() throws SQLException {
  ResultSet rs = stmt.executeQuery("SELECT TIMESTAMP '1000-01-01 00:00:00 UTC'"); rs.next();
  assertEquals(Instant.class, rs.getObject(1, Instant.class).getClass());
  // actual: java.lang.String "1000-01-01 00:00:00.0" (not an Instant)
}

Environment: macOS, Java 17, Databricks JDBC 3.4.4 (also reproduced on 3.1.1), Databricks SQL warehouse (DBSQL 2026.44).

Dominant language
Java
Stars
41
Forks
49
Avg merge
1d 7h
Merged PRs (30d)
27

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from databricks/databricks-jdbc

All issues in databricks/databricks-jdbc

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.