From e903434c2994a6fa1e2b93c10b5c83a3944f2a43 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 25 Aug 2026 13:16:36 +0200 Subject: [PATCH 1/2] Fix NPE on materialising nested arrays or arrays of maps or structs with `NULL` elements. Null elements in strongly typed nested arrays are valid elements and must be returned as such. This fixes the instance of checking by adding a null-check on all nested branches (arrays of arrays, arrays of maps and arrays of structs). This closes #1658. Signed-off-by: Michael Simons --- .../jdbc/api/impl/DatabricksArray.java | 6 ++ .../e2e/ComplexTypeQueryTests.java | 83 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/main/java/com/databricks/jdbc/api/impl/DatabricksArray.java b/src/main/java/com/databricks/jdbc/api/impl/DatabricksArray.java index c914b33339..89dd6ab188 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/DatabricksArray.java +++ b/src/main/java/com/databricks/jdbc/api/impl/DatabricksArray.java @@ -54,6 +54,8 @@ private Object[] convertElements(List elements, String elementType) { convertedElements[i] = new DatabricksStruct((Map) element, elementType); } else if (element instanceof DatabricksStruct) { convertedElements[i] = element; + } else if (element == null) { + convertedElements[i] = null; } else { throw new DatabricksDriverException( "Expected a Map for STRUCT but found: " + element.getClass().getSimpleName(), @@ -64,6 +66,8 @@ private Object[] convertElements(List elements, String elementType) { convertedElements[i] = new DatabricksArray((List) element, elementType); } else if (element instanceof DatabricksArray) { convertedElements[i] = element; + } else if (element == null) { + convertedElements[i] = null; } else { throw new DatabricksDriverException( "Expected a List for ARRAY but found: " + element.getClass().getSimpleName(), @@ -74,6 +78,8 @@ private Object[] convertElements(List elements, String elementType) { convertedElements[i] = new DatabricksMap<>((Map) element, elementType); } else if (element instanceof DatabricksMap) { convertedElements[i] = element; + } else if (element == null) { + convertedElements[i] = null; } else { throw new DatabricksDriverException( "Expected a Map for MAP but found: " + element.getClass().getSimpleName(), diff --git a/src/test/java/com/databricks/jdbc/integration/e2e/ComplexTypeQueryTests.java b/src/test/java/com/databricks/jdbc/integration/e2e/ComplexTypeQueryTests.java index 0ef768e291..9f9e9ed844 100644 --- a/src/test/java/com/databricks/jdbc/integration/e2e/ComplexTypeQueryTests.java +++ b/src/test/java/com/databricks/jdbc/integration/e2e/ComplexTypeQueryTests.java @@ -190,6 +190,89 @@ void testEmptyArray(int thriftVal, int complexSupport) throws SQLException { } } + @ParameterizedTest + @CsvSource({"0,0", "1,0", "0,1", "1,1"}) + void testNestedArrayWithNullElement(int thriftVal, int complexSupport) throws SQLException { + setupConnection(thriftVal, complexSupport); + + String sql = "SELECT array(null, array('Hello'))::ARRAY> AS arr"; + ResultSet rs = executeQuery(connection, sql); + assertNotNull(rs); + while (rs.next()) { + if (complexSupport == 1) { + Array arr = rs.getArray("arr"); + assertNotNull(arr); + Object[] elements = (Object[]) arr.getArray(); + assertEquals(2, elements.length); + assertNull(elements[0]); + assertInstanceOf(Array.class, elements[1]); + Object[] innerElements = (Object[]) ((Array) elements[1]).getArray(); + assertEquals(1, innerElements.length); + assertEquals("Hello", innerElements[0]); + } else { + assertThrows(SQLException.class, () -> rs.getArray("arr")); + Object obj = rs.getObject("arr"); + assertInstanceOf(String.class, obj); + String text = (String) obj; + assertFalse(text.isEmpty()); + } + } + } + + @ParameterizedTest + @CsvSource({"0,0", "1,0", "0,1", "1,1"}) + void testArrayOfMapsWithNullElement(int thriftVal, int complexSupport) throws SQLException { + setupConnection(thriftVal, complexSupport); + + String sql = + "SELECT array(null, map('red', 1, 'green', 2))::ARRAY> AS arr"; + ResultSet rs = executeQuery(connection, sql); + assertNotNull(rs); + while (rs.next()) { + if (complexSupport == 1) { + Array arr = rs.getArray("arr"); + assertNotNull(arr); + Object[] elements = (Object[]) arr.getArray(); + assertEquals(2, elements.length); + assertNull(elements[0]); + assertInstanceOf(Map.class, elements[1]); + } else { + assertThrows(SQLException.class, () -> rs.getArray("arr")); + Object obj = rs.getObject("arr"); + assertInstanceOf(String.class, obj); + String text = (String) obj; + assertFalse(text.isEmpty()); + } + } + } + + @ParameterizedTest + @CsvSource({"0,0", "1,0", "0,1", "1,1"}) + void testArrayOfStructsWithNullElement(int thriftVal, int complexSupport) throws SQLException { + setupConnection(thriftVal, complexSupport); + + String sql = + "SELECT array(null, struct('Spark', 5))::ARRAY> AS arr"; + ResultSet rs = executeQuery(connection, sql); + assertNotNull(rs); + while (rs.next()) { + if (complexSupport == 1) { + Array arr = rs.getArray("arr"); + assertNotNull(arr); + Object[] elements = (Object[]) arr.getArray(); + assertEquals(2, elements.length); + assertNull(elements[0]); + assertInstanceOf(Struct.class, elements[1]); + } else { + assertThrows(SQLException.class, () -> rs.getArray("arr")); + Object obj = rs.getObject("arr"); + assertInstanceOf(String.class, obj); + String text = (String) obj; + assertFalse(text.isEmpty()); + } + } + } + @ParameterizedTest @CsvSource({"0,0", "1,0", "0,1", "1,1"}) void testNullArray(int thriftVal, int complexSupport) throws SQLException { From be8aab1d7a96d47a6c59b8239176b1ea17e52c83 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 26 Aug 2026 11:47:24 +0200 Subject: [PATCH 2/2] Add changelog entry. Signed-off-by: Michael Simons --- NEXT_CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 8e44c82b1c..cfb2a21b31 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -18,6 +18,8 @@ - Fixed connecting with an unsupported `AuthMech` (e.g. `AuthMech=99`) intermittently failing with an internal `IllegalStateException: Recursive update` or `StackOverflowError` on both the SEA and Thrift paths. The value is now validated at connect time and rejected deterministically with a `SQLException` (`SQLState=INPUT_VALIDATION_ERROR`). - Improved SEA connection-failure error messages. + +- Fixed `NullPointerException` being thrown when materializing an array containing nested object types (other arrays, structs or maps) as `DatabricksArray` when some or all elements are literal `null`. --- *Note: When making changes, please add your change under the appropriate section with a brief description.*