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-11915-69be95c1d925bd876c71c11ec6cb2a646d54a1ad in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
commit 4218aa177e0c668dfb08c94b8dccd95eecedd0cc Author: NingLin Liu <[email protected]> AuthorDate: Mon Oct 26 15:21:04 2026 +0000 fix(arrow-avro): keep nulls when reading strings as Utf8View (#11915) # Which issue does this PR close? - Closes #11735 # Rationale for this change With `ReaderBuilder::with_utf8_view(true)`, the `StringView` arm of `Decoder::flush` built a `StringArray` with the correct null buffer, then copied it into a `Vec<&str>` that replaced every NULL with `""` before building the `StringViewArray`. The resulting array had no null buffer, so every NULL of a nullable string field came back as a non-null empty string (`null_count == 0`), while the same file read with `with_utf8_view(false)` was correct. # What changes are included in this PR? - Build the `StringViewArray` with `StringViewArray::from(&StringArray)`, which keeps the null buffer and creates views into the existing values buffer instead of copying each string through a `Vec<&str>`. (#11737 proposed the same approach but was closed by its author without merging; this picks the fix back up.) # Are these changes tested? Yes. Added `test_utf8_view_keeps_nulls_of_nullable_strings`, which writes `["a", null, "", null, "b"]` with `AvroWriter` and reads it back with `with_utf8_view(true)`. It fails on main (`null_count` 0 instead of 2) and passes with the fix. `cargo test -p arrow-avro` and `cargo clippy -p arrow-avro --all-targets --all-features -- -D warnings` pass. # Are there any user-facing changes? Yes, a bug fix: with `with_utf8_view(true)`, NULL strings are now read as nulls instead of empty strings. No API changes. This PR was prepared with AI assistance and reviewed/tested by the author. --- arrow-avro/src/reader/mod.rs | 22 ++++++++++++++++++++++ arrow-avro/src/reader/record.rs | 13 ++----------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/arrow-avro/src/reader/mod.rs b/arrow-avro/src/reader/mod.rs index 2faa90c83e..51a160d9a1 100644 --- a/arrow-avro/src/reader/mod.rs +++ b/arrow-avro/src/reader/mod.rs @@ -1550,6 +1550,28 @@ mod test { arrow::compute::concat_batches(&schema, &batches).unwrap() } + #[test] + fn test_utf8_view_keeps_nulls_of_nullable_strings() { + let schema = Schema::new(vec![Field::new("s", DataType::Utf8, true)]); + let values = vec![Some("a"), None, Some(""), None, Some("b")]; + let col: ArrayRef = Arc::new(StringArray::from(values.clone())); + let batch = RecordBatch::try_new(Arc::new(schema.clone()), vec![col]).unwrap(); + let mut writer = AvroWriter::new(Vec::<u8>::new(), schema).unwrap(); + writer.write(&batch).unwrap(); + writer.finish().unwrap(); + let bytes = writer.into_inner(); + + let reader = ReaderBuilder::new() + .with_utf8_view(true) + .build(Cursor::new(bytes)) + .unwrap(); + let batches = reader.collect::<Result<Vec<_>, _>>().unwrap(); + assert_eq!(batches.len(), 1); + let col = batches[0].column(0).as_string_view(); + assert_eq!(col.null_count(), 2); + assert_eq!(col, &StringViewArray::from(values)); + } + #[test] fn test_block_sync_marker_mismatch_errors() { let path = arrow_test_data("avro/alltypes_plain.avro"); diff --git a/arrow-avro/src/reader/record.rs b/arrow-avro/src/reader/record.rs index 5b8f4e607f..815c445742 100644 --- a/arrow-avro/src/reader/record.rs +++ b/arrow-avro/src/reader/record.rs @@ -1687,17 +1687,8 @@ impl Decoder { Self::StringView(offsets, values) => { let offsets = flush_offsets(offsets); let values = flush_values(values); - let array = StringArray::try_new(offsets, values.into(), nulls.clone())?; - let values: Vec<&str> = (0..array.len()) - .map(|i| { - if array.is_valid(i) { - array.value(i) - } else { - "" - } - }) - .collect(); - Arc::new(StringViewArray::from(values)) + let array = StringArray::try_new(offsets, values.into(), nulls)?; + Arc::new(StringViewArray::from(&array)) } Self::Array(field, offsets, values) => { let values = values.flush(None)?;
