From 2d42f4cfc0f93d70338369e22b3e3b896f688f35 Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Wed, 4 Mar 2026 05:07:22 +0000 Subject: [PATCH 1/3] Fix Date fields in complex types returning epoch day integers instead of date strings When Arrow serializes DATE fields inside nested types (ARRAY, STRUCT, MAP), getObject() returns epoch day integers rather than ISO-8601 date strings. ComplexDataTypeParser.convertPrimitive() now falls back to parsing epoch day integers via LocalDate.ofEpochDay() when Date.valueOf() fails. Closes #1247 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Vikrant Puppala --- .../jdbc/api/impl/ComplexDataTypeParser.java | 9 ++- .../api/impl/ComplexDataTypeParserTest.java | 75 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java index 834ffd304..490583672 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java +++ b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java @@ -15,6 +15,7 @@ import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; @@ -204,7 +205,13 @@ private Object convertPrimitive(String text, String type) { case DatabricksTypeUtil.BOOLEAN: return Boolean.parseBoolean(text); case DatabricksTypeUtil.DATE: - return Date.valueOf(text); + try { + return Date.valueOf(text); + } catch (IllegalArgumentException e) { + // Arrow serializes DATE fields in nested types as epoch day integers. + // Fall back to parsing as epoch day count (days since 1970-01-01). + return Date.valueOf(LocalDate.ofEpochDay(Long.parseLong(text))); + } case DatabricksTypeUtil.TIMESTAMP: return parseTimestamp(text); case DatabricksTypeUtil.TIME: diff --git a/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java b/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java index def3f19a5..d0d730f1b 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java @@ -150,6 +150,81 @@ void testComplexPrimitiveTimestampWithOffset() throws DatabricksParsingException } } + @Test + void testDateAsEpochDayInStruct() throws DatabricksParsingException { + // Reproduces GitHub issue #1247: Date fields within ARRAY are serialized + // as epoch day integers instead of ISO-8601 strings when coming from Arrow. + // Arrow's getObject() on nested types returns epoch day integers for DATE fields. + // 20487 = epoch day for 2026-02-03 + String json = "[{\"event_date\":20487}]"; + + DatabricksArray dbArray = + parser.parseJsonStringToDbArray(json, "ARRAY>"); + assertNotNull(dbArray); + + try { + Object[] elements = (Object[]) dbArray.getArray(); + assertEquals(1, elements.length); + DatabricksStruct struct = (DatabricksStruct) elements[0]; + Object[] attrs = struct.getAttributes(); + assertEquals(1, attrs.length); + assertEquals(Date.valueOf("2026-02-03"), attrs[0]); + } catch (Exception e) { + fail("Should not throw: " + e.getMessage()); + } + } + + @Test + void testDateAsEpochDayInArray() throws DatabricksParsingException { + // DATE inside a plain ARRAY — Arrow returns epoch day integers + String json = "[20487, 20488]"; + + DatabricksArray dbArray = parser.parseJsonStringToDbArray(json, "ARRAY"); + assertNotNull(dbArray); + + try { + Object[] elements = (Object[]) dbArray.getArray(); + assertEquals(2, elements.length); + assertEquals(Date.valueOf("2026-02-03"), elements[0]); + assertEquals(Date.valueOf("2026-02-04"), elements[1]); + } catch (Exception e) { + fail("Should not throw: " + e.getMessage()); + } + } + + @Test + void testDateAsEpochDayInMap() throws DatabricksParsingException { + // DATE as value in a MAP — Arrow returns epoch day integers + String json = "{\"key1\":20487, \"key2\":20488}"; + + DatabricksMap dbMap = parser.parseJsonStringToDbMap(json, "MAP"); + assertNotNull(dbMap); + + assertEquals(Date.valueOf("2026-02-03"), dbMap.get("key1")); + assertEquals(Date.valueOf("2026-02-04"), dbMap.get("key2")); + } + + @Test + void testDateAsStringInStruct() throws DatabricksParsingException { + // Ensure ISO-8601 date strings still work in nested structs + String json = "[{\"event_date\":\"2026-02-03\"}]"; + + DatabricksArray dbArray = + parser.parseJsonStringToDbArray(json, "ARRAY>"); + assertNotNull(dbArray); + + try { + Object[] elements = (Object[]) dbArray.getArray(); + assertEquals(1, elements.length); + DatabricksStruct struct = (DatabricksStruct) elements[0]; + Object[] attrs = struct.getAttributes(); + assertEquals(1, attrs.length); + assertEquals(Date.valueOf("2026-02-03"), attrs[0]); + } catch (Exception e) { + fail("Should not throw: " + e.getMessage()); + } + } + @Test void testFormatComplexTypeString_withMapType() { String jsonString = "[{\"key\":1,\"value\":2},{\"key\":3,\"value\":4}]"; From 9f07d27f561d040a8a81e7b83515df3afae23c2d Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Wed, 4 Mar 2026 05:30:41 +0000 Subject: [PATCH 2/3] Address review: preserve original exception for invalid date strings, add changelog - Wrap epoch-day fallback in try-catch so non-numeric invalid date strings (e.g., "2026/02/03") rethrow the original IllegalArgumentException instead of a NumberFormatException. - Add test for invalid date string error behavior. - Add NEXT_CHANGELOG.md entry for the user-facing bug fix. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Vikrant Puppala --- NEXT_CHANGELOG.md | 1 + .../jdbc/api/impl/ComplexDataTypeParser.java | 6 +++++- .../jdbc/api/impl/ComplexDataTypeParserTest.java | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index e5d88e859..17a3a0ac7 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -15,6 +15,7 @@ - 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. - Fixed `fetchAutoCommitStateFromServer()` to accept both `"1"`/`"0"` and `"true"`/`"false"` responses from `SET AUTOCOMMIT` query, since different server implementations return different formats. - 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. +- Fixed Date fields within complex types (ARRAY, STRUCT, MAP) being returned as epoch day integers instead of proper date values. --- *Note: When making changes, please add your change under the appropriate section diff --git a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java index 490583672..c6c965cf9 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java +++ b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java @@ -210,7 +210,11 @@ private Object convertPrimitive(String text, String type) { } catch (IllegalArgumentException e) { // Arrow serializes DATE fields in nested types as epoch day integers. // Fall back to parsing as epoch day count (days since 1970-01-01). - return Date.valueOf(LocalDate.ofEpochDay(Long.parseLong(text))); + try { + return Date.valueOf(LocalDate.ofEpochDay(Long.parseLong(text))); + } catch (NumberFormatException nfe) { + throw e; + } } case DatabricksTypeUtil.TIMESTAMP: return parseTimestamp(text); diff --git a/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java b/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java index d0d730f1b..a935952f3 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/ComplexDataTypeParserTest.java @@ -204,6 +204,17 @@ void testDateAsEpochDayInMap() throws DatabricksParsingException { assertEquals(Date.valueOf("2026-02-04"), dbMap.get("key2")); } + @Test + void testInvalidDateStringInStructThrowsOriginalException() { + // Non-numeric invalid date string should throw IllegalArgumentException, not + // NumberFormatException + String json = "[{\"event_date\":\"2026/02/03\"}]"; + + assertThrows( + IllegalArgumentException.class, + () -> parser.parseJsonStringToDbArray(json, "ARRAY>")); + } + @Test void testDateAsStringInStruct() throws DatabricksParsingException { // Ensure ISO-8601 date strings still work in nested structs From 529df0f0ddfd561711ef288bb62717a50583da6a Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Mon, 9 Mar 2026 13:45:46 +0000 Subject: [PATCH 3/3] Address review: catch DateTimeException and add error logging for DATE parsing Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Vikrant Puppala --- .../com/databricks/jdbc/api/impl/ComplexDataTypeParser.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java index c6c965cf9..787ba6e6a 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java +++ b/src/main/java/com/databricks/jdbc/api/impl/ComplexDataTypeParser.java @@ -15,6 +15,7 @@ import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.time.DateTimeException; import java.time.LocalDate; import java.util.ArrayList; import java.util.Iterator; @@ -212,7 +213,8 @@ private Object convertPrimitive(String text, String type) { // Fall back to parsing as epoch day count (days since 1970-01-01). try { return Date.valueOf(LocalDate.ofEpochDay(Long.parseLong(text))); - } catch (NumberFormatException nfe) { + } catch (NumberFormatException | DateTimeException nfe) { + LOGGER.error(e, "Failed to parse DATE value '{}' as epoch day integer", text); throw e; } }