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/10/22 15:34:42 UTC
(spark-connect-swift) branch main updated: [SPARK-60472] Fix `collect` to throw an error instead of misreading compressed Arrow batches
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 9b32afb [SPARK-60472] Fix `collect` to throw an error instead of misreading compressed Arrow batches
9b32afb is described below
commit 9b32afb97425282be5c796625bc9af1129967016
Author: Dongjoon Hyun <do...@apache.org>
AuthorDate: Thu Oct 22 08:34:35 2026 -0700
[SPARK-60472] Fix `collect` to throw an error instead of misreading compressed Arrow batches
### What changes were proposed in this pull request?
This PR fixes `DataFrame` actions to throw `SparkConnectError.unsupportedOperation` instead of returning wrong values or crashing when the Spark Connect server sends compressed Arrow batches.
- `ArrowReader` returns an error for a record batch with `compression` instead of reading the compressed buffers as they are.
- `DataFrame` actions check the header of each Arrow batch first and throw `unsupportedOperation` with the codec name and the workaround, `spark.sql.execution.arrow.compression.codec=none`.
### Why are the changes needed?
Since Spark 4.1.0, the server compresses the buffers of Arrow batches when [spark.sql.execution.arrow.compression.codec](https://github.com/apache/spark/blob/v4.1.0/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala#L4046) is `zstd` or `lz4`. [ArrowBatchIterator](https://github.com/apache/spark/blob/v4.1.0/sql/core/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowConverters.scala#L118) of `v4.1.0` (2025-12-11) passes the codec to `VectorUnloader`. The serv [...]
```swift
try await spark.conf.set("spark.sql.execution.arrow.compression.codec", "zstd")
try await spark.range(10).count() // -1
try await spark.range(10).collect() // Wrong values like -1
try await spark.range(3).show() // Fatal error: UnsafeBufferPointer with negative count
```
Like the Scala client, which fails with `IllegalArgumentException: Please add arrow-compression module to use CommonsCompressionFactory for ZSTD`, this PR does not support decompression. PySpark decompresses the batches with PyArrow.
### Does this PR introduce _any_ user-facing change?
Yes. With Spark 4.1+ and a compression codec, `collect()`, `collect(as:)`, `head()`, `first()`, `take()`, `tail()`, `count()`, and `show()` throw the following error instead of returning wrong values or crashing. Results without buffers, e.g., `df.select()`, also throw it.
```
unsupportedOperation: Unsupported Arrow compression codec: zstd. Set `spark.sql.execution.arrow.compression.codec` to `none`.
```
There is no change when the codec is `none`, which is the default.
### How was this patch tested?
Added `RecordBatchTests.readCompressed`, which does not require a Spark Connect server, and `DataFrameTests.arrowCompression` for Spark 4.1+. Verified against an `apache/spark:4.2.0` Connect server.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 5.5
Closes #618 from dongjoon-hyun/SPARK-60472.
Authored-by: Dongjoon Hyun <do...@apache.org>
Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
Sources/SparkConnect/ArrowReader.swift | 4 ++++
Sources/SparkConnect/DataFrame+Actions.swift | 15 +++++++++++++++
Tests/SparkConnectTests/DataFrameTests.swift | 19 +++++++++++++++++++
Tests/SparkConnectTests/RecordBatchTests.swift | 25 +++++++++++++++++++++++++
4 files changed, 63 insertions(+)
diff --git a/Sources/SparkConnect/ArrowReader.swift b/Sources/SparkConnect/ArrowReader.swift
index cde7c28..4d31401 100644
--- a/Sources/SparkConnect/ArrowReader.swift
+++ b/Sources/SparkConnect/ArrowReader.swift
@@ -284,6 +284,10 @@ public class ArrowReader { // swiftlint:disable:this type_body_length
guard let length = UInt(exactly: recordBatch.length) else {
return .failure(.invalid("Invalid record batch length: \(recordBatch.length)"))
}
+ // Compressed buffers cannot be read as they are, and decompression is not supported.
+ if let compression = recordBatch.compression {
+ return .failure(.invalid("Unsupported record batch compression codec: \(compression.codec)"))
+ }
var columns: [ArrowArrayHolder] = []
let batchData = RecordBatchData(recordBatch, schema: schema)
let loadInfo = DataLoadInfo(
diff --git a/Sources/SparkConnect/DataFrame+Actions.swift b/Sources/SparkConnect/DataFrame+Actions.swift
index 3dd484c..f6dd55a 100644
--- a/Sources/SparkConnect/DataFrame+Actions.swift
+++ b/Sources/SparkConnect/DataFrame+Actions.swift
@@ -22,6 +22,7 @@ import FoundationEssentials
#else
import Foundation
#endif
+import FlatBuffers
import GRPCProtobuf
import Synchronization
@@ -100,6 +101,20 @@ extension DataFrame {
let dataBodySize = totalSize - pos - 8
let dataBody = Data(ipcStreamBytes[pos..<(pos + dataBodySize)])
+ // Spark 4.1+ compresses buffers if `spark.sql.execution.arrow.compression.codec` is set.
+ let message = org_apache_arrow_flatbuf_Message.getRootAsMessage(
+ bb: ByteBuffer(data: dataHeader))
+ if message.headerType == .recordbatch,
+ let recordBatch = message.header(type: org_apache_arrow_flatbuf_RecordBatch.self),
+ let compression = recordBatch.compression
+ {
+ throw SparkConnectError.unsupportedOperation(
+ SparkConnectError.Details(
+ message:
+ "Unsupported Arrow compression codec: \(compression.codec). "
+ + "Set `spark.sql.execution.arrow.compression.codec` to `none`."))
+ }
+
// Read ArrowBatches
try batches.withLock { batches in
let reader = ArrowReader()
diff --git a/Tests/SparkConnectTests/DataFrameTests.swift b/Tests/SparkConnectTests/DataFrameTests.swift
index 8687a1b..7710d7a 100644
--- a/Tests/SparkConnectTests/DataFrameTests.swift
+++ b/Tests/SparkConnectTests/DataFrameTests.swift
@@ -774,6 +774,25 @@ struct DataFrameTests {
await spark.stop()
}
+ @Test
+ func arrowCompression() async throws {
+ let spark = try await SparkSession.builder.getOrCreate()
+ if await isSparkVersionAtLeast(spark.version, "4.1") {
+ let df = try await spark.range(10)
+ let expected = try await df.collect()
+ for codec in ["zstd", "lz4"] {
+ try await spark.conf.set("spark.sql.execution.arrow.compression.codec", codec)
+ let error = await #expect(throws: SparkConnectError.self) { try await df.collect() }
+ #expect(error == SparkConnectError.UnsupportedOperation)
+ #expect(error?.message.contains(codec) == true)
+ #expect(error?.message.contains("spark.sql.execution.arrow.compression.codec") == true)
+ }
+ try await spark.conf.unset("spark.sql.execution.arrow.compression.codec")
+ #expect(try await df.collect() == expected)
+ }
+ await spark.stop()
+ }
+
@Test
func isEmpty() async throws {
let spark = try await SparkSession.builder.getOrCreate()
diff --git a/Tests/SparkConnectTests/RecordBatchTests.swift b/Tests/SparkConnectTests/RecordBatchTests.swift
index 639ec8d..d9fba66 100644
--- a/Tests/SparkConnectTests/RecordBatchTests.swift
+++ b/Tests/SparkConnectTests/RecordBatchTests.swift
@@ -22,6 +22,7 @@ import FoundationEssentials
#else
import Foundation
#endif
+import FlatBuffers
import Testing
@testable import SparkConnect
@@ -54,4 +55,28 @@ struct RecordBatchTests {
#expect(result.batches[0].columnCount == 0)
#expect(result.batches[0].length == 3)
}
+
+ @Test
+ func readCompressed() throws {
+ // Spark 4.1+ compresses the buffers if `spark.sql.execution.arrow.compression.codec` is set.
+ let schemaMessage = try ArrowWriter().toMessage(ArrowSchema.Builder().finish()).get()
+ for codec in [org_apache_arrow_flatbuf_CompressionType.lz4Frame, .zstd] {
+ var fbb = FlatBufferBuilder()
+ let compression = org_apache_arrow_flatbuf_BodyCompression.createBodyCompression(
+ &fbb, codec: codec)
+ let recordBatch = org_apache_arrow_flatbuf_RecordBatch.createRecordBatch(
+ &fbb, length: 3, compressionOffset: compression)
+ let message = org_apache_arrow_flatbuf_Message.createMessage(
+ &fbb, version: .max, headerType: .recordbatch, headerOffset: recordBatch)
+ fbb.finish(offset: message)
+
+ let reader = ArrowReader()
+ let result = ArrowReader.makeArrowReaderResult()
+ try reader.fromMessage(schemaMessage, dataBody: Data(), result: result).get()
+ #expect(throws: ArrowError.self) {
+ try reader.fromMessage(fbb.data, dataBody: Data(), result: result).get()
+ }
+ #expect(result.batches.isEmpty)
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org