This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 40699a23ff Fix IPC roundtrip for empty RunArray slices (#11361)
40699a23ff is described below
commit 40699a23ff7bf65a70a707a726ef3ffe50b178df
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Sun Oct 4 07:19:17 2026 +0000
Fix IPC roundtrip for empty RunArray slices (#11361)
# Which issue does this PR close?
Closes #11364.
# Rationale for this change
Writing an empty slice of a nonempty `RunArray` succeeds, but reading
the resulting IPC batch fails because the writer emits a run end of
zero. For example, slicing run ends `[2, 5]` and values `[10, 20]` with
`slice(2, 0)` produces this error. Run ends must be strictly positive.
# What changes are included in this PR?
Normalize empty run arrays to zero-offset arrays with empty run-end and
value children before calculating physical run indices. Add regression
tests for Int16, Int32, and Int64 run ends, every legal empty-slice
offset in the example array, already-empty arrays, and both Stream and
File IPC roundtrips.
Implementation and regression tests were generated with OpenAI Codex and
reviewed and validated with the checks below.
# Are these changes tested?
- Default arrow-ipc library tests: 134 passed.
- All-features arrow-ipc library tests: 140 passed.
- New regression tests with `arrow-data/force_validate`: 3 passed.
- Clippy for arrow-ipc with all targets and all features, with warnings
denied: passed.
- Workspace formatting and diff whitespace checks: passed.
The full all-features suite with `arrow-data/force_validate` has six
failures in existing reader tests that intentionally construct invalid
arrays. The same six failures were reproduced on the unchanged base
commit; all new tests pass with forced validation.
# Are there any user-facing changes?
Empty RunArray slices now roundtrip through Stream and File IPC
successfully. No public API changes.
---
arrow-ipc/src/writer.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/arrow-ipc/src/writer.rs b/arrow-ipc/src/writer.rs
index 30555a738a..c435aa3147 100644
--- a/arrow-ipc/src/writer.rs
+++ b/arrow-ipc/src/writer.rs
@@ -1279,6 +1279,11 @@ pub(crate) fn unslice_run_array(arr: ArrayData) ->
Result<ArrayData, ArrowError>
fn into_zero_offset_run_array<R: RunEndIndexType>(
run_array: RunArray<R>,
) -> Result<RunArray<R>, ArrowError> {
+ // Empty slices have no physical runs, regardless of their offset.
+ if run_array.is_empty() {
+ return Ok(ArrayData::new_empty(run_array.data_type()).into());
+ }
+
let run_ends = run_array.run_ends();
if run_ends.offset() == 0 && run_ends.max_value() == run_ends.len() {
return Ok(run_array);
@@ -3764,6 +3769,56 @@ mod tests {
assert_eq!(batch, batch2);
}
+ fn assert_empty_run_array_roundtrip<R: RunEndIndexType>() {
+ let run_ends = PrimitiveArray::<R>::from_iter_values(
+ [2, 5]
+ .into_iter()
+ .map(|v| R::Native::from_usize(v).unwrap()),
+ );
+ let values = Int32Array::from(vec![10, 20]);
+ let array = RunArray::<R>::try_new(&run_ends, &values).unwrap();
+ let empty =
RunArray::<R>::from(ArrayData::new_empty(array.data_type()));
+
+ for source in [&array, &empty] {
+ for offset in 0..=source.len() {
+ let sliced = source.slice(offset, 0);
+ let batch = RecordBatch::try_from_iter(vec![("run",
Arc::new(sliced) as ArrayRef)])
+ .unwrap();
+ for decoded in [
+ deserialize_stream(serialize_stream(&batch)),
+ deserialize_file(serialize_file(&batch)),
+ ] {
+ assert_eq!(decoded, batch);
+ let data = decoded.column(0).to_data();
+ data.validate_full().unwrap();
+ assert_eq!(data.offset(), 0);
+ assert!(data.child_data().iter().all(ArrayData::is_empty));
+ }
+
+ let normalized =
into_zero_offset_run_array(source.slice(offset, 0)).unwrap();
+ normalized.to_data().validate_full().unwrap();
+ assert_eq!(normalized.offset(), 0);
+ assert!(normalized.run_ends().values().is_empty());
+ assert!(normalized.values().is_empty());
+ }
+ }
+ }
+
+ #[test]
+ fn test_empty_run_array_roundtrip_i16() {
+ assert_empty_run_array_roundtrip::<Int16Type>();
+ }
+
+ #[test]
+ fn test_empty_run_array_roundtrip_i32() {
+ assert_empty_run_array_roundtrip::<Int32Type>();
+ }
+
+ #[test]
+ fn test_empty_run_array_roundtrip_i64() {
+ assert_empty_run_array_roundtrip::<Int64Type>();
+ }
+
#[test]
fn test_run_array_unslice() {
let total_len = 80;