You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/07/25 02:55:43 UTC
(spark-connect-swift) branch main updated: [SPARK-58344] Support Arrow `Null` type to fix `collect()` crash on void columns
This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/spark-connect-swift.git
The following commit(s) were added to refs/heads/main by this push:
new 99f6d39 [SPARK-58344] Support Arrow `Null` type to fix `collect()` crash on void columns
99f6d39 is described below
commit 99f6d3978de9018b56223bc2d42c46afb9cc1342
Author: Dongjoon Hyun <do...@apache.org>
AuthorDate: Fri Jul 24 19:55:37 2026 -0700
[SPARK-58344] Support Arrow `Null` type to fix `collect()` crash on void columns
### What changes were proposed in this pull request?
This PR adds Arrow `Null` type support to the vendored Arrow reader so that collecting a result with a NULL-typed (`void`) column returns `nil` values instead of crashing.
- `ArrowType.swift`: add `ArrowType.ArrowNull` and handle `.null` in `getStride()` (previously `fatalError`).
- `ArrowReaderHelper.swift`: map the flatbuffer `.null` field type in `findArrowType` and add `makeNullHolder` which builds an `ArrowData` with no buffers.
- `ArrowReader.swift`: add a `.null` branch in `loadField` (`loadNullData`) that consumes only the field node, since Arrow `Null` arrays carry no validity or data buffers.
- `ArrowArray.swift`: add `NullArray` (always returns `nil`) and a `.null` case in `ArrowArrayHolderImpl.loadArray` for null columns nested inside structs.
- `ArrowData.swift`: `isNull` returns `true` for the null type instead of accessing the non-existent validity buffer.
### Why are the changes needed?
Any query whose result schema contains a `NullType` column crashes the client process with `SIGTRAP`.
```swift
let spark = try await SparkSession.builder.getOrCreate()
_ = try await spark.sql("SELECT null").collect()
// Fatal error: Unexpectedly found nil while unwrapping an Optional value
```
The reader did not recognize the Arrow `Null` type, so `loadSchema` failed and `DataFrame.execute()` proceeded with a `nil` `messageSchema`, crashing on a force-unwrap in `ArrowReader.fromMessage`. This is easy to hit, e.g. `SELECT :x` with a null named parameter.
### Does this PR introduce _any_ user-facing change?
Yes, this is a bug fix. Previously, `collect()` on a result containing a `void` column crashed the process; now it returns `nil` for those values (e.g. `SELECT null` returns `[Row(nil)]`).
### How was this patch tested?
Pass the CIs with a newly added test case.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Fable 5
Closes #445 from dongjoon-hyun/SPARK-58344.
Authored-by: Dongjoon Hyun <do...@apache.org>
Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
Sources/SparkConnect/ArrowArray.swift | 9 +++++++++
Sources/SparkConnect/ArrowData.swift | 3 +++
Sources/SparkConnect/ArrowReader.swift | 16 ++++++++++++++++
Sources/SparkConnect/ArrowReaderHelper.swift | 17 +++++++++++++++++
Sources/SparkConnect/ArrowType.swift | 3 +++
Tests/SparkConnectTests/DataFrameTests.swift | 10 ++++++++++
6 files changed, 58 insertions(+)
diff --git a/Sources/SparkConnect/ArrowArray.swift b/Sources/SparkConnect/ArrowArray.swift
index 8154f90..dff8efd 100644
--- a/Sources/SparkConnect/ArrowArray.swift
+++ b/Sources/SparkConnect/ArrowArray.swift
@@ -128,6 +128,8 @@ public class ArrowArrayHolderImpl: ArrowArrayHolder {
return try ArrowArrayHolderImpl(NestedArray(with))
case .list:
return try ArrowArrayHolderImpl(NestedArray(with))
+ case .null:
+ return try ArrowArrayHolderImpl(NullArray(with))
default:
throw ArrowError.invalid("Array not found for type: \(arrowType)")
}
@@ -174,6 +176,13 @@ public class ArrowArray<T>: AsString, AnyArray {
}
}
+/// @nodoc
+public class NullArray: ArrowArray<Any> {
+ public override subscript(_ index: UInt) -> Any? {
+ return nil
+ }
+}
+
/// @nodoc
public class FixedArray<T>: ArrowArray<T> {
public override subscript(_ index: UInt) -> T? {
diff --git a/Sources/SparkConnect/ArrowData.swift b/Sources/SparkConnect/ArrowData.swift
index 0c747ee..97974d2 100644
--- a/Sources/SparkConnect/ArrowData.swift
+++ b/Sources/SparkConnect/ArrowData.swift
@@ -70,6 +70,9 @@ public class ArrowData {
}
public func isNull(_ at: UInt) -> Bool {
+ if self.type.id == .null {
+ return true
+ }
let nullBuffer = buffers[0]
return nullBuffer.length > 0 && !BitUtility.isSet(at, buffer: nullBuffer)
}
diff --git a/Sources/SparkConnect/ArrowReader.swift b/Sources/SparkConnect/ArrowReader.swift
index 76c2470..c51569e 100644
--- a/Sources/SparkConnect/ArrowReader.swift
+++ b/Sources/SparkConnect/ArrowReader.swift
@@ -168,6 +168,20 @@ public class ArrowReader { // swiftlint:disable:this type_body_length
}
}
+ private func loadNullData(
+ _ loadInfo: DataLoadInfo,
+ field: org_apache_arrow_flatbuf_Field
+ )
+ -> Result<ArrowArrayHolder, ArrowError>
+ {
+ guard let node = loadInfo.batchData.nextNode() else {
+ return .failure(.invalid("Node not found"))
+ }
+
+ // Arrow Null arrays carry no validity or data buffers.
+ return makeNullHolder(UInt(node.length))
+ }
+
private func loadPrimitiveData(
_ loadInfo: DataLoadInfo,
field: org_apache_arrow_flatbuf_Field
@@ -244,6 +258,8 @@ public class ArrowReader { // swiftlint:disable:this type_body_length
-> Result<ArrowArrayHolder, ArrowError>
{
switch field.typeType {
+ case .null:
+ return loadNullData(loadInfo, field: field)
case .struct_:
return loadStructData(loadInfo, field: field)
case .list:
diff --git a/Sources/SparkConnect/ArrowReaderHelper.swift b/Sources/SparkConnect/ArrowReaderHelper.swift
index 5642346..eeefdfc 100644
--- a/Sources/SparkConnect/ArrowReaderHelper.swift
+++ b/Sources/SparkConnect/ArrowReaderHelper.swift
@@ -150,6 +150,21 @@ private func makeBoolHolder(
}
}
+func makeNullHolder(
+ _ length: UInt
+) -> Result<ArrowArrayHolder, ArrowError> {
+ do {
+ let arrowType = ArrowType(ArrowType.ArrowNull)
+ let arrowData = try ArrowData(
+ arrowType, buffers: [], children: [], nullCount: length, length: length)
+ return .success(ArrowArrayHolderImpl(try NullArray(arrowData)))
+ } catch let error as ArrowError {
+ return .failure(error)
+ } catch {
+ return .failure(.unknownError("\(error)"))
+ }
+}
+
private func makeFixedHolder<T>(
_: T.Type, field: ArrowField, buffers: [ArrowBuffer],
nullCount: UInt
@@ -277,6 +292,8 @@ func findArrowType( // swiftlint:disable:this cyclomatic_complexity function_bo
) -> ArrowType {
let type = field.typeType
switch type {
+ case .null:
+ return ArrowType(ArrowType.ArrowNull)
case .int:
let intType = field.type(type: org_apache_arrow_flatbuf_Int.self)!
let bitWidth = intType.bitWidth
diff --git a/Sources/SparkConnect/ArrowType.swift b/Sources/SparkConnect/ArrowType.swift
index 7d47a90..2ca02ec 100644
--- a/Sources/SparkConnect/ArrowType.swift
+++ b/Sources/SparkConnect/ArrowType.swift
@@ -240,6 +240,7 @@ public class ArrowType {
public static let ArrowTimestamp = Info.timeInfo(ArrowTypeId.timestamp)
public static let ArrowStruct = Info.complexInfo(ArrowTypeId.strct)
public static let ArrowList = Info.complexInfo(ArrowTypeId.list)
+ public static let ArrowNull = Info.primitiveInfo(ArrowTypeId.null)
public init(_ info: ArrowType.Info) {
self.info = info
@@ -375,6 +376,8 @@ public class ArrowType {
return MemoryLayout<Int8>.stride
case .strct, .list:
return 0
+ case .null:
+ return 0
default:
fatalError("Stride requested for unknown type: \(self)")
}
diff --git a/Tests/SparkConnectTests/DataFrameTests.swift b/Tests/SparkConnectTests/DataFrameTests.swift
index e495b13..e052095 100644
--- a/Tests/SparkConnectTests/DataFrameTests.swift
+++ b/Tests/SparkConnectTests/DataFrameTests.swift
@@ -190,6 +190,16 @@ struct DataFrameTests {
await spark.stop()
}
+ @Test
+ func collectNull() async throws {
+ let spark = try await SparkSession.builder.getOrCreate()
+ #expect(try await spark.sql("SELECT null").collect() == [Row(nil)])
+ #expect(try await spark.sql("SELECT null, 1").collect() == [Row(nil, 1)])
+ #expect(try await spark.sql("SELECT null FROM RANGE(2)").collect() == [Row(nil), Row(nil)])
+ #expect(try await spark.sql("SELECT struct(null)").collect().count == 1)
+ await spark.stop()
+ }
+
@Test
func selectNone() async throws {
let spark = try await SparkSession.builder.getOrCreate()
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org