Skip to content

Commit c7291f5

Browse files
Fix Date fields in complex types returning epoch day integers (#1248)
## Summary - Fixes #1247: Date fields within `ARRAY<STRUCT>`, `ARRAY<DATE>`, `MAP<*,DATE>`, and other complex types were serialized as epoch day integers instead of proper `java.sql.Date` objects. - Arrow's `getObject()` on nested types returns epoch day integers for DATE fields. `ComplexDataTypeParser.convertPrimitive()` now falls back to parsing epoch day integers via `LocalDate.ofEpochDay()` when `Date.valueOf()` fails on non-ISO-8601 input. - Non-numeric invalid date strings preserve the original `IllegalArgumentException`. ## Test plan - [x] `testDateAsEpochDayInStruct` — verifies DATE epoch day integers in `ARRAY<STRUCT<event_date:DATE>>` - [x] `testDateAsEpochDayInArray` — verifies DATE epoch day integers in `ARRAY<DATE>` - [x] `testDateAsEpochDayInMap` — verifies DATE epoch day integers in `MAP<STRING,DATE>` - [x] `testDateAsStringInStruct` — verifies ISO-8601 date strings still work (no regression) - [x] `testInvalidDateStringInStructThrowsOriginalException` — verifies error behavior for invalid strings - [x] All 18 `ComplexDataTypeParserTest` tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 717cc27 commit c7291f5

3 files changed

Lines changed: 101 additions & 1 deletion

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- Fixed `rollback()` to throw `SQLException` when called in auto-commit mode (no active transaction), aligning with JDBC spec. Previously it silently sent a ROLLBACK command to the server.
1616
- Fixed `fetchAutoCommitStateFromServer()` to accept both `"1"`/`"0"` and `"true"`/`"false"` responses from `SET AUTOCOMMIT` query, since different server implementations return different formats.
1717
- Fixed socket leak in SDK HTTP client that prevented CRaC checkpointing. The SDK's connection pool was not shut down on `connection.close()`, leaving TCP sockets open.
18+
- Fixed Date fields within complex types (ARRAY, STRUCT, MAP) being returned as epoch day integers instead of proper date values.
1819

1920
---
2021
*Note: When making changes, please add your change under the appropriate section

src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import java.sql.Date;
1616
import java.sql.Time;
1717
import java.sql.Timestamp;
18+
import java.time.DateTimeException;
19+
import java.time.LocalDate;
1820
import java.util.ArrayList;
1921
import java.util.Iterator;
2022
import java.util.LinkedHashMap;
@@ -204,7 +206,18 @@ private Object convertPrimitive(String text, String type) {
204206
case DatabricksTypeUtil.BOOLEAN:
205207
return Boolean.parseBoolean(text);
206208
case DatabricksTypeUtil.DATE:
207-
return Date.valueOf(text);
209+
try {
210+
return Date.valueOf(text);
211+
} catch (IllegalArgumentException e) {
212+
// Arrow serializes DATE fields in nested types as epoch day integers.
213+
// Fall back to parsing as epoch day count (days since 1970-01-01).
214+
try {
215+
return Date.valueOf(LocalDate.ofEpochDay(Long.parseLong(text)));
216+
} catch (NumberFormatException | DateTimeException nfe) {
217+
LOGGER.error(e, "Failed to parse DATE value '{}' as epoch day integer", text);
218+
throw e;
219+
}
220+
}
208221
case DatabricksTypeUtil.TIMESTAMP:
209222
return parseTimestamp(text);
210223
case DatabricksTypeUtil.TIME:

src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,92 @@ void testComplexPrimitiveTimestampWithOffset() throws DatabricksParsingException
150150
}
151151
}
152152

153+
@Test
154+
void testDateAsEpochDayInStruct() throws DatabricksParsingException {
155+
// Reproduces GitHub issue #1247: Date fields within ARRAY<STRUCT> are serialized
156+
// as epoch day integers instead of ISO-8601 strings when coming from Arrow.
157+
// Arrow's getObject() on nested types returns epoch day integers for DATE fields.
158+
// 20487 = epoch day for 2026-02-03
159+
String json = "[{\"event_date\":20487}]";
160+
161+
DatabricksArray dbArray =
162+
parser.parseJsonStringToDbArray(json, "ARRAY<STRUCT<event_date:DATE>>");
163+
assertNotNull(dbArray);
164+
165+
try {
166+
Object[] elements = (Object[]) dbArray.getArray();
167+
assertEquals(1, elements.length);
168+
DatabricksStruct struct = (DatabricksStruct) elements[0];
169+
Object[] attrs = struct.getAttributes();
170+
assertEquals(1, attrs.length);
171+
assertEquals(Date.valueOf("2026-02-03"), attrs[0]);
172+
} catch (Exception e) {
173+
fail("Should not throw: " + e.getMessage());
174+
}
175+
}
176+
177+
@Test
178+
void testDateAsEpochDayInArray() throws DatabricksParsingException {
179+
// DATE inside a plain ARRAY — Arrow returns epoch day integers
180+
String json = "[20487, 20488]";
181+
182+
DatabricksArray dbArray = parser.parseJsonStringToDbArray(json, "ARRAY<DATE>");
183+
assertNotNull(dbArray);
184+
185+
try {
186+
Object[] elements = (Object[]) dbArray.getArray();
187+
assertEquals(2, elements.length);
188+
assertEquals(Date.valueOf("2026-02-03"), elements[0]);
189+
assertEquals(Date.valueOf("2026-02-04"), elements[1]);
190+
} catch (Exception e) {
191+
fail("Should not throw: " + e.getMessage());
192+
}
193+
}
194+
195+
@Test
196+
void testDateAsEpochDayInMap() throws DatabricksParsingException {
197+
// DATE as value in a MAP — Arrow returns epoch day integers
198+
String json = "{\"key1\":20487, \"key2\":20488}";
199+
200+
DatabricksMap<String, Object> dbMap = parser.parseJsonStringToDbMap(json, "MAP<STRING,DATE>");
201+
assertNotNull(dbMap);
202+
203+
assertEquals(Date.valueOf("2026-02-03"), dbMap.get("key1"));
204+
assertEquals(Date.valueOf("2026-02-04"), dbMap.get("key2"));
205+
}
206+
207+
@Test
208+
void testInvalidDateStringInStructThrowsOriginalException() {
209+
// Non-numeric invalid date string should throw IllegalArgumentException, not
210+
// NumberFormatException
211+
String json = "[{\"event_date\":\"2026/02/03\"}]";
212+
213+
assertThrows(
214+
IllegalArgumentException.class,
215+
() -> parser.parseJsonStringToDbArray(json, "ARRAY<STRUCT<event_date:DATE>>"));
216+
}
217+
218+
@Test
219+
void testDateAsStringInStruct() throws DatabricksParsingException {
220+
// Ensure ISO-8601 date strings still work in nested structs
221+
String json = "[{\"event_date\":\"2026-02-03\"}]";
222+
223+
DatabricksArray dbArray =
224+
parser.parseJsonStringToDbArray(json, "ARRAY<STRUCT<event_date:DATE>>");
225+
assertNotNull(dbArray);
226+
227+
try {
228+
Object[] elements = (Object[]) dbArray.getArray();
229+
assertEquals(1, elements.length);
230+
DatabricksStruct struct = (DatabricksStruct) elements[0];
231+
Object[] attrs = struct.getAttributes();
232+
assertEquals(1, attrs.length);
233+
assertEquals(Date.valueOf("2026-02-03"), attrs[0]);
234+
} catch (Exception e) {
235+
fail("Should not throw: " + e.getMessage());
236+
}
237+
}
238+
153239
@Test
154240
void testFormatComplexTypeString_withMapType() {
155241
String jsonString = "[{\"key\":1,\"value\":2},{\"key\":3,\"value\":4}]";

0 commit comments

Comments
 (0)