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

Aperta
#1,779 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@cathleeny ci sta già lavorando.

Dal 11/11/2026.

  • #1781 di @cathleeny — aperta

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
25/100
Tipo di issue
Funzionalità
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
java
Ambito
database

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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).

Lingua principale
Java
Stelle
41
Fork
49
Merge medio
1g 7h
PR unite (30g)
27

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di databricks/databricks-jdbc

Tutte le issue di databricks/databricks-jdbc

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.