This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11361-cac30c2d586bde9b3c13a3f1166ddba4fbf87cd1 in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
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;
