diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index a8173af77..6c27fbc1a 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -23,6 +23,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.* 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 c914b3333..89dd6ab18 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 0ef768e29..9f9e9ed84 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 {