diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 32c4eb1af3..43cce78527 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -12,6 +12,7 @@ ### Fixed - Fix: unsupported data types in `setObject(int,Object,int targetSqlType)` method in PreparedStatement +- Fix: Added explicit null check for Arrow value vector when the value is empty, and Arrow null checking is disabled. --- *Note: When making changes, please add your change under the appropriate section with a brief description.* \ No newline at end of file diff --git a/src/main/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverter.java b/src/main/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverter.java index d394afaada..54cc7419aa 100644 --- a/src/main/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverter.java +++ b/src/main/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverter.java @@ -63,6 +63,10 @@ public static Object convert( ColumnInfoTypeName requiredType, String arrowMetadata) throws DatabricksSQLException { + // check isNull before getting the object from the vector + if (columnVector.isNull(vectorIndex)) { + return null; + } Object object = columnVector.getObject(vectorIndex); if (arrowMetadata != null) { if (arrowMetadata.startsWith(ARRAY)) { diff --git a/src/test/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverterTest.java b/src/test/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverterTest.java index 8cc7800ba2..a6a1167d4c 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverterTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/converters/ArrowToJavaObjectConverterTest.java @@ -1,5 +1,6 @@ package com.databricks.jdbc.api.impl.converters; +import static com.databricks.jdbc.api.impl.converters.ArrowToJavaObjectConverter.convert; import static com.databricks.jdbc.api.impl.converters.ArrowToJavaObjectConverter.getZoneIdFromTimeZoneOpt; import static com.databricks.jdbc.common.util.DatabricksTypeUtil.VARIANT; import static org.junit.jupiter.api.Assertions.*; @@ -43,6 +44,89 @@ public void testNullObjectConversion() throws SQLException { assertNull(convertedObject); } + @Test + public void testNullHandlingInVarCharVector() throws Exception { + // Create a VarCharVector with 3 values: non-null, null, and empty string + disableArrowNullChecking(); + VarCharVector vector = new VarCharVector("varCharVector", this.bufferAllocator); + vector.allocateNew(4); + + // Set first value: "hello" + vector.set(0, "hello".getBytes()); + + // Second value: null (don't set it, which makes it null by default) + + // Third value: empty string (explicitly set as "") + vector.set(2, "".getBytes()); + + // Fourth value: set to null + vector.setNull(3); + + // Set vector value count + vector.setValueCount(4); + + // Case 1: Non-null value + assertFalse(vector.isNull(0)); + assertEquals("hello", convert(vector, 0, ColumnInfoTypeName.STRING, "STRING")); + + // Case 2: Null value + assertTrue(vector.isNull(1)); + assertNull(convert(vector, 1, ColumnInfoTypeName.STRING, "STRING")); + + // Case 3: Empty string (should not be treated as null) + assertFalse(vector.isNull(2)); + assertEquals( + "", + convert( + vector, + 2, + ColumnInfoTypeName.STRING, + "STRING")); // Empty string should be empty, not null + + // Case 4: Explicitly set to null + assertTrue(vector.isNull(3)); + String valueWithoutCheck = (String) convert(vector, 3, ColumnInfoTypeName.STRING, "STRING"); + // This assertion is expected to fail - it shows the problem when isNull check is removed + assertNull(valueWithoutCheck); + + enableArrowNullChecking(); + } + + private void disableArrowNullChecking() { + System.setProperty("arrow.enable_null_check_for_get", "false"); + } + + private void enableArrowNullChecking() { + System.setProperty("arrow.enable_null_check_for_get", "true"); + } + + @Test + public void testByteVectorWithNullChecks() throws Exception { + TinyIntVector vector = new TinyIntVector("tinyIntVector", this.bufferAllocator); + vector.allocateNew(3); + + // First value: explicitly set to null + vector.setNull(0); + + // Second value: skip setting it, which makes it null by default + + // Third value: set to 0 + vector.set(2, 0); + + vector.setValueCount(3); + + // Test our converter with proper null handling + assertTrue(vector.isNull(0)); + assertNull(convert(vector, 0, ColumnInfoTypeName.BYTE, "BYTE")); + + assertTrue(vector.isNull(1)); + assertNull(convert(vector, 1, ColumnInfoTypeName.BYTE, "BYTE")); + + // The zero value should still be correctly identified as 0, not null + assertFalse(vector.isNull(2)); + assertEquals((byte) 0, convert(vector, 2, ColumnInfoTypeName.BYTE, "BYTE")); + } + @Test public void testByteConversion() throws SQLException { TinyIntVector tinyIntVector = new TinyIntVector("tinyIntVector", this.bufferAllocator);