| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| use crate::decode_utf8_spark_lossy; | |
| use arrow::array::{ | |
| downcast_dictionary_array, Array, ArrayRef, FixedSizeListArray, GenericListArray, | |
| GenericStringArray, GenericStringBuilder, MapArray, OffsetSizeTrait, StructArray, | |
| }; | |
| use arrow::datatypes::DataType; | |
| use arrow::error::ArrowError; | |
| use std::sync::Arc; | |
| /// Ensure every `Utf8`/`LargeUtf8` array reachable from `array` holds valid UTF-8, decoding invalid | |
| /// bytes the way Spark renders `StringType`. Returns the same `Arc` (zero-copy) when nothing needed | |
| /// decoding. Used at the JVM->native FFI import boundary, where arrow's `from_ffi` builds string | |
| /// arrays via `new_unchecked` and does not validate UTF-8. Unsupported string-bearing Arrow types | |
| /// fail closed rather than preserving unchecked string data. | |
| pub fn decode_string_arrays(array: &ArrayRef) -> Result<ArrayRef, ArrowError> { | |
| match array.data_type() { | |
| DataType::Utf8 => decode_generic_string::<i32>(array), | |
| DataType::LargeUtf8 => decode_generic_string::<i64>(array), | |
| DataType::Dictionary(_, value_type) => { | |
| if !data_type_contains_string(value_type) { | |
| return Ok(Arc::clone(array)); | |
| } | |
| // Capture the original Arc before `downcast_dictionary_array!` shadows `array`, so the | |
| // unchanged branch returns it verbatim, preserving the zero-copy contract that the | |
| // Struct/List/Map arms rely on via `Arc::ptr_eq`. | |
| let original = Arc::clone(array); | |
| downcast_dictionary_array!( | |
| array => { | |
| let values = array.values(); | |
| let decoded = decode_string_arrays(values)?; | |
| if Arc::ptr_eq(&decoded, values) { | |
| Ok(original) | |
| } else { | |
| Ok(Arc::new(array.with_values(decoded))) | |
| } | |
| } | |
| t => unreachable!("dictionary key type validated by Arrow: {t}"), | |
| ) | |
| } | |
| DataType::Struct(fields) => { | |
| let s = array | |
| .as_any() | |
| .downcast_ref::<StructArray>() | |
| .expect("data type checked by caller"); | |
| let mut changed = false; | |
| let mut columns = Vec::with_capacity(s.num_columns()); | |
| for col in s.columns() { | |
| let decoded = decode_string_arrays(col)?; | |
| changed |= !Arc::ptr_eq(&decoded, col); | |
| columns.push(decoded); | |
| } | |
| if !changed { | |
| return Ok(Arc::clone(array)); | |
| } | |
| Ok(Arc::new(StructArray::new( | |
| fields.clone(), | |
| columns, | |
| s.nulls().cloned(), | |
| ))) | |
| } | |
| DataType::List(_) => decode_list::<i32>(array), | |
| DataType::LargeList(_) => decode_list::<i64>(array), | |
| DataType::FixedSizeList(field, size) => { | |
| let list = array | |
| .as_any() | |
| .downcast_ref::<FixedSizeListArray>() | |
| .expect("data type checked by caller"); | |
| let values = list.values(); | |
| let decoded = decode_string_arrays(values)?; | |
| if Arc::ptr_eq(&decoded, values) { | |
| return Ok(Arc::clone(array)); | |
| } | |
| Ok(Arc::new(FixedSizeListArray::try_new( | |
| Arc::clone(field), | |
| *size, | |
| decoded, | |
| list.nulls().cloned(), | |
| )?)) | |
| } | |
| DataType::Map(field, ordered) => { | |
| let map = array | |
| .as_any() | |
| .downcast_ref::<MapArray>() | |
| .expect("data type checked by caller"); | |
| let entries = map.entries(); | |
| let mut changed = false; | |
| let mut columns = Vec::with_capacity(entries.num_columns()); | |
| for col in entries.columns() { | |
| let decoded = decode_string_arrays(col)?; | |
| changed |= !Arc::ptr_eq(&decoded, col); | |
| columns.push(decoded); | |
| } | |
| if !changed { | |
| return Ok(Arc::clone(array)); | |
| } | |
| let decoded_entries = | |
| StructArray::new(entries.fields().clone(), columns, entries.nulls().cloned()); | |
| Ok(Arc::new(MapArray::try_new( | |
| Arc::clone(field), | |
| map.offsets().clone(), | |
| decoded_entries, | |
| map.nulls().cloned(), | |
| *ordered, | |
| )?)) | |
| } | |
| // Spark's current JVM producers emit StringType as Utf8/LargeUtf8 and the supported | |
| // containers above. Fail closed if a different string-bearing representation reaches this | |
| // boundary: passing it through would preserve the invalid UTF-8 this function exists to | |
| // remove. Keep `data_type_contains_string` exhaustive so an Arrow upgrade adding another | |
| // data type forces this safety audit to be revisited. | |
| data_type => { | |
| if data_type_contains_string(data_type) { | |
| Err(unsupported_string_type(data_type)) | |
| } else { | |
| Ok(Arc::clone(array)) | |
| } | |
| } | |
| } | |
| } | |
| fn unsupported_string_type(data_type: &DataType) -> ArrowError { | |
| ArrowError::NotYetImplemented(format!( | |
| "decoding FFI-imported string data in {data_type} is not supported" | |
| )) | |
| } | |
| /// Whether `data_type` is, or can contain, a logical string representation. | |
| /// | |
| /// This match is deliberately exhaustive: when Arrow adds a data type, the compiler should force | |
| /// us to decide whether it can carry strings before `decode_string_arrays` passes it through. | |
| fn data_type_contains_string(data_type: &DataType) -> bool { | |
| match data_type { | |
| DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => true, | |
| DataType::List(field) | |
| | DataType::ListView(field) | |
| | DataType::FixedSizeList(field, _) | |
| | DataType::LargeList(field) | |
| | DataType::LargeListView(field) => data_type_contains_string(field.data_type()), | |
| DataType::Struct(fields) => fields | |
| .iter() | |
| .any(|field| data_type_contains_string(field.data_type())), | |
| DataType::Union(fields, _) => fields | |
| .iter() | |
| .any(|(_, field)| data_type_contains_string(field.data_type())), | |
| DataType::Dictionary(_, value_type) => data_type_contains_string(value_type), | |
| DataType::Map(field, _) => data_type_contains_string(field.data_type()), | |
| DataType::RunEndEncoded(_, values) => data_type_contains_string(values.data_type()), | |
| DataType::Null | |
| | DataType::Boolean | |
| | DataType::Int8 | |
| | DataType::Int16 | |
| | DataType::Int32 | |
| | DataType::Int64 | |
| | DataType::UInt8 | |
| | DataType::UInt16 | |
| | DataType::UInt32 | |
| | DataType::UInt64 | |
| | DataType::Float16 | |
| | DataType::Float32 | |
| | DataType::Float64 | |
| | DataType::Timestamp(_, _) | |
| | DataType::Date32 | |
| | DataType::Date64 | |
| | DataType::Time32(_) | |
| | DataType::Time64(_) | |
| | DataType::Duration(_) | |
| | DataType::Interval(_) | |
| | DataType::Binary | |
| | DataType::FixedSizeBinary(_) | |
| | DataType::LargeBinary | |
| | DataType::BinaryView | |
| | DataType::Decimal32(_, _) | |
| | DataType::Decimal64(_, _) | |
| | DataType::Decimal128(_, _) | |
| | DataType::Decimal256(_, _) => false, | |
| } | |
| } | |
| fn decode_generic_string<O: OffsetSizeTrait>(array: &ArrayRef) -> Result<ArrayRef, ArrowError> { | |
| let arr = array | |
| .as_any() | |
| .downcast_ref::<GenericStringArray<O>>() | |
| .expect("data type checked by caller"); | |
| let len = arr.len(); | |
| if len == 0 { | |
| return Ok(Arc::clone(array)); | |
| } | |
| let values: &[u8] = arr.value_data(); | |
| let offsets = arr.value_offsets(); // &[O], length == len + 1 | |
| let start = offsets[0].as_usize(); | |
| let end = offsets[len].as_usize(); | |
| // Fast path: the used byte range parses as UTF-8 AND no element boundary splits a codepoint. | |
| // Both are required: a whole-buffer-valid "é" (C3 A9) with per-element offsets [0,1,2] yields | |
| // element slices `C3` and `A9`, each invalid, and `value()` would decode them unchecked (UB). | |
| // | |
| // Every imported string column pays this pass, so it uses simdutf8 rather than | |
| // `std::str::from_utf8`: std is fast on ASCII but falls back to a byte-at-a-time loop on | |
| // non-ASCII text, which made multibyte columns several times slower to import. The `basic` | |
| // validator is enough because an invalid buffer takes the slow path below, which finds the | |
| // bad bytes itself. | |
| if simdutf8::basic::from_utf8(&values[start..end]).is_ok() { | |
| let mut boundaries_ok = true; | |
| for off in &offsets[1..len] { | |
| let o = off.as_usize(); | |
| // A boundary landing on a UTF-8 continuation byte (0b10xx_xxxx) splits a codepoint. | |
| if o < values.len() && (values[o] & 0xC0) == 0x80 { | |
| boundaries_ok = false; | |
| break; | |
| } | |
| } | |
| if boundaries_ok { | |
| return Ok(Arc::clone(array)); | |
| } | |
| } | |
| // Slow path: rebuild element-by-element via the Spark-lossy decoder. We slice the raw values | |
| // buffer directly rather than calling `arr.value(i)`, which uses `from_utf8_unchecked`. | |
| let mut builder = GenericStringBuilder::<O>::with_capacity(len, end - start); | |
| for i in 0..len { | |
| if arr.is_null(i) { | |
| builder.append_null(); | |
| } else { | |
| let s = offsets[i].as_usize(); | |
| let e = offsets[i + 1].as_usize(); | |
| builder.append_value(decode_utf8_spark_lossy(&values[s..e])); | |
| } | |
| } | |
| Ok(Arc::new(builder.finish())) | |
| } | |
| fn decode_list<O: OffsetSizeTrait>(array: &ArrayRef) -> Result<ArrayRef, ArrowError> { | |
| let list = array | |
| .as_any() | |
| .downcast_ref::<GenericListArray<O>>() | |
| .expect("data type checked by caller"); | |
| let values = list.values(); | |
| let decoded = decode_string_arrays(values)?; | |
| if Arc::ptr_eq(&decoded, values) { | |
| return Ok(Arc::clone(array)); | |
| } | |
| let field = match array.data_type() { | |
| DataType::List(f) | DataType::LargeList(f) => Arc::clone(f), | |
| _ => unreachable!("decode_list called on non-list"), | |
| }; | |
| Ok(Arc::new(GenericListArray::<O>::try_new( | |
| field, | |
| list.offsets().clone(), | |
| decoded, | |
| list.nulls().cloned(), | |
| )?)) | |
| } | |
| #[cfg(test)] | |
| mod walker_tests { | |
| use super::{data_type_contains_string, decode_string_arrays}; | |
| use arrow::array::{ | |
| make_array, Array, ArrayData, ArrayRef, LargeStringArray, StringArray, StringViewArray, | |
| }; | |
| use arrow::buffer::Buffer; | |
| use arrow::datatypes::DataType; | |
| use arrow::error::ArrowError; | |
| use std::sync::Arc; | |
| /// Build a (possibly invalid) Utf8 array from raw offsets + value bytes, the way an FFI import | |
| /// would deliver it (no validation). `build_unchecked` mirrors arrow's `from_ffi`. | |
| fn utf8_unchecked(offsets: &[i32], values: &[u8], len: usize) -> ArrayRef { | |
| let data = unsafe { | |
| ArrayData::builder(DataType::Utf8) | |
| .len(len) | |
| .add_buffer(Buffer::from_slice_ref(offsets)) | |
| .add_buffer(Buffer::from(values.to_vec())) | |
| .build_unchecked() | |
| }; | |
| make_array(data) | |
| } | |
| #[test] | |
| fn valid_utf8_is_zero_copy() { | |
| let input: ArrayRef = Arc::new(StringArray::from(vec!["a", "é", "🦀"])); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| assert!( | |
| Arc::ptr_eq(&input, &out), | |
| "valid input must be returned unchanged" | |
| ); | |
| } | |
| #[test] | |
| fn unsupported_string_representation_fails_closed() { | |
| let input: ArrayRef = Arc::new(StringViewArray::from(vec!["valid"])); | |
| let err = decode_string_arrays(&input).unwrap_err(); | |
| assert!(matches!( | |
| err, | |
| ArrowError::NotYetImplemented(message) if message.contains("Utf8View") | |
| )); | |
| } | |
| #[test] | |
| fn invalid_bytes_decode_to_replacement() { | |
| // element 0 = [0xFF, 0x41] -> "\u{FFFD}A", element 1 = [0x42] -> "B" | |
| let input = utf8_unchecked(&[0, 2, 3], &[0xFF, 0x41, 0x42], 2); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| let s = out.as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(s.value(0), "\u{FFFD}A"); | |
| assert_eq!(s.value(1), "B"); | |
| } | |
| #[test] | |
| fn split_codepoint_boundary_is_rebuilt() { | |
| // whole buffer "é" (C3 A9) is valid UTF-8, but offsets split it into two invalid slices. | |
| let input = utf8_unchecked(&[0, 1, 2], &[0xC3, 0xA9], 2); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| let s = out.as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(s.value(0), "\u{FFFD}"); | |
| assert_eq!(s.value(1), "\u{FFFD}"); | |
| } | |
| #[test] | |
| fn nulls_are_preserved() { | |
| let data = unsafe { | |
| ArrayData::builder(DataType::Utf8) | |
| .len(3) | |
| .null_count(1) | |
| .null_bit_buffer(Some(Buffer::from(vec![0b0000_0101_u8]))) | |
| .add_buffer(Buffer::from_slice_ref([0_i32, 1, 2, 3])) | |
| .add_buffer(Buffer::from(vec![0xff, 0xfe, b'b'])) | |
| .build_unchecked() | |
| }; | |
| let input = make_array(data); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| let s = out.as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(s.value(0), "\u{FFFD}"); | |
| assert!(s.is_null(1)); | |
| assert_eq!(s.value(2), "b"); | |
| } | |
| #[test] | |
| fn large_utf8_invalid_decodes() { | |
| let data = unsafe { | |
| ArrayData::builder(DataType::LargeUtf8) | |
| .len(1) | |
| .add_buffer(Buffer::from_slice_ref([0i64, 1])) | |
| .add_buffer(Buffer::from(vec![0xFFu8])) | |
| .build_unchecked() | |
| }; | |
| let out = decode_string_arrays(&make_array(data)).unwrap(); | |
| let s = out.as_any().downcast_ref::<LargeStringArray>().unwrap(); | |
| assert_eq!(s.value(0), "\u{FFFD}"); | |
| } | |
| use arrow::array::{ | |
| DictionaryArray, FixedSizeListArray, Int32Array, ListArray, MapArray, StructArray, | |
| }; | |
| use arrow::datatypes::{Field, Fields, Int32Type}; | |
| #[test] | |
| fn detects_unhandled_nested_string_representations() { | |
| let string_field = Arc::new(Field::new("item", DataType::Utf8, true)); | |
| let list_view = DataType::ListView(Arc::clone(&string_field)); | |
| let large_list_view = DataType::LargeListView(Arc::clone(&string_field)); | |
| assert!(data_type_contains_string(&list_view)); | |
| assert!(data_type_contains_string(&large_list_view)); | |
| let run_ends = Arc::new(Field::new("run_ends", DataType::Int32, false)); | |
| let run_end_encoded = DataType::RunEndEncoded(run_ends, string_field); | |
| assert!(data_type_contains_string(&run_end_encoded)); | |
| let int_field = Arc::new(Field::new("item", DataType::Int32, true)); | |
| assert!(!data_type_contains_string(&DataType::ListView(int_field))); | |
| } | |
| /// An invalid Utf8 leaf ["\u{FFFD}"] built from raw bytes. | |
| fn invalid_leaf() -> ArrayRef { | |
| utf8_unchecked(&[0, 1], &[0xFF], 1) | |
| } | |
| #[test] | |
| fn dictionary_values_are_decoded() { | |
| let values = invalid_leaf(); | |
| let keys = Int32Array::from(vec![0, 0]); | |
| let dict: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::new(keys, values)); | |
| let out = decode_string_arrays(&dict).unwrap(); | |
| let d = out | |
| .as_any() | |
| .downcast_ref::<DictionaryArray<Int32Type>>() | |
| .unwrap(); | |
| let vals = d.values().as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(vals.value(0), "\u{FFFD}"); | |
| } | |
| #[test] | |
| fn struct_field_is_decoded() { | |
| let field = Arc::new(Field::new("s", DataType::Utf8, true)); | |
| let input: ArrayRef = Arc::new(StructArray::new( | |
| Fields::from(vec![field]), | |
| vec![invalid_leaf()], | |
| None, | |
| )); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| let s = out.as_any().downcast_ref::<StructArray>().unwrap(); | |
| let col = s.column(0).as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(col.value(0), "\u{FFFD}"); | |
| } | |
| #[test] | |
| fn list_values_are_decoded() { | |
| let field = Arc::new(Field::new("item", DataType::Utf8, true)); | |
| let offsets = arrow::buffer::OffsetBuffer::new(vec![0i32, 1].into()); | |
| let input: ArrayRef = | |
| Arc::new(ListArray::try_new(field, offsets, invalid_leaf(), None).unwrap()); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| let l = out.as_any().downcast_ref::<ListArray>().unwrap(); | |
| let vals = l.values().as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(vals.value(0), "\u{FFFD}"); | |
| } | |
| #[test] | |
| fn valid_struct_is_zero_copy() { | |
| let field = Arc::new(Field::new("s", DataType::Utf8, true)); | |
| let leaf: ArrayRef = Arc::new(StringArray::from(vec!["ok"])); | |
| let input: ArrayRef = Arc::new(StructArray::new( | |
| Fields::from(vec![field]), | |
| vec![leaf], | |
| None, | |
| )); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| assert!( | |
| Arc::ptr_eq(&input, &out), | |
| "all-valid nested input must be unchanged" | |
| ); | |
| } | |
| #[test] | |
| fn dictionary_valid_values_are_zero_copy() { | |
| let values: ArrayRef = Arc::new(StringArray::from(vec!["a", "b"])); | |
| let keys = Int32Array::from(vec![0, 1, 0]); | |
| let input: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::new(keys, values)); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| assert!( | |
| Arc::ptr_eq(&input, &out), | |
| "dictionary with all-valid values must be returned as the original Arc" | |
| ); | |
| } | |
| #[test] | |
| fn fixed_size_list_values_are_decoded() { | |
| let field = Arc::new(Field::new("item", DataType::Utf8, true)); | |
| let input: ArrayRef = | |
| Arc::new(FixedSizeListArray::try_new(field, 1, invalid_leaf(), None).unwrap()); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| let l = out.as_any().downcast_ref::<FixedSizeListArray>().unwrap(); | |
| let vals = l.values().as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(vals.value(0), "\u{FFFD}"); | |
| } | |
| #[test] | |
| fn fixed_size_list_valid_is_zero_copy() { | |
| let field = Arc::new(Field::new("item", DataType::Utf8, true)); | |
| let leaf: ArrayRef = Arc::new(StringArray::from(vec!["ok"])); | |
| let input: ArrayRef = Arc::new(FixedSizeListArray::try_new(field, 1, leaf, None).unwrap()); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| assert!( | |
| Arc::ptr_eq(&input, &out), | |
| "fixed-size list with all-valid values must be unchanged" | |
| ); | |
| } | |
| /// Build a Map array whose single entry maps `key` -> `values` (a Utf8 leaf array of length 1). | |
| fn build_map(key: &str, values: ArrayRef) -> ArrayRef { | |
| let entries_fields = Fields::from(vec![ | |
| Arc::new(Field::new("keys", DataType::Utf8, false)), | |
| Arc::new(Field::new("values", DataType::Utf8, true)), | |
| ]); | |
| let keys: ArrayRef = Arc::new(StringArray::from(vec![key])); | |
| let entries = StructArray::new(entries_fields.clone(), vec![keys, values], None); | |
| let map_field = Arc::new(Field::new( | |
| "entries", | |
| DataType::Struct(entries_fields), | |
| false, | |
| )); | |
| let offsets = arrow::buffer::OffsetBuffer::new(vec![0i32, 1].into()); | |
| Arc::new(MapArray::try_new(map_field, offsets, entries, None, false).unwrap()) | |
| } | |
| #[test] | |
| fn map_values_are_decoded() { | |
| let input = build_map("k", invalid_leaf()); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| let m = out.as_any().downcast_ref::<MapArray>().unwrap(); | |
| let values = m | |
| .entries() | |
| .column(1) | |
| .as_any() | |
| .downcast_ref::<StringArray>() | |
| .unwrap(); | |
| assert_eq!(values.value(0), "\u{FFFD}"); | |
| } | |
| #[test] | |
| fn map_valid_is_zero_copy() { | |
| let values: ArrayRef = Arc::new(StringArray::from(vec!["ok"])); | |
| let input = build_map("k", values); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| assert!( | |
| Arc::ptr_eq(&input, &out), | |
| "map with all-valid values must be unchanged" | |
| ); | |
| } | |
| #[test] | |
| fn trailing_empty_string_offset_is_handled() { | |
| // element 0 = "ab", element 1 = "" (its offset lands exactly at values.len()). | |
| let input = utf8_unchecked(&[0, 2, 2], b"ab", 2); | |
| let out = decode_string_arrays(&input).unwrap(); | |
| assert!( | |
| Arc::ptr_eq(&input, &out), | |
| "valid input with a trailing empty element must be returned unchanged" | |
| ); | |
| let s = out.as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(s.value(0), "ab"); | |
| assert_eq!(s.value(1), ""); | |
| } | |
| #[test] | |
| fn sliced_array_is_decoded() { | |
| // Slicing shifts offsets[0] away from 0; the fast path must still handle this correctly. | |
| let base: ArrayRef = Arc::new(StringArray::from(vec!["aa", "bb", "cc"])); | |
| let sliced: ArrayRef = base.slice(1, 2); | |
| let out = decode_string_arrays(&sliced).unwrap(); | |
| // Slicing already produces a new Arc distinct from `base`, so ptr_eq against `base` isn't | |
| // meaningful here; what matters is that the fast path recognizes the slice as valid and | |
| // returns it unchanged (same Arc as `sliced`) rather than rebuilding it. | |
| assert!( | |
| Arc::ptr_eq(&sliced, &out), | |
| "valid sliced input must be returned unchanged" | |
| ); | |
| let s = out.as_any().downcast_ref::<StringArray>().unwrap(); | |
| assert_eq!(s.value(0), "bb"); | |
| assert_eq!(s.value(1), "cc"); | |
| } | |
| } |