diff --git a/cpp/src/arrow/array/array_list_test.cc b/cpp/src/arrow/array/array_list_test.cc index 4901a1e6fa6a..7236ce9b947d 100644 --- a/cpp/src/arrow/array/array_list_test.cc +++ b/cpp/src/arrow/array/array_list_test.cc @@ -1049,6 +1049,35 @@ class TestListConversions : public ::testing::Test { ASSERT_OK(result->ValidateFull()); AssertArraysEqual(*expected_list_wo_nulls, *result, /*verbose=*/true); } + + template + void DoTestListFromSlicedListView() { + using SrcListViewArrayClass = typename TypeTraits::ArrayType; + using DestListArrayClass = typename TypeTraits::ArrayType; + auto list_view_type = std::make_shared(int32()); + auto list_type = std::make_shared(int32()); + + auto list_view = + ArrayFromJSON(list_view_type, "[[1, 2], [3], null, [], [4], [5, 6], null, [7]]"); + + auto check = [&](const Array& sliced, const std::shared_ptr& expected) { + ASSERT_OK_AND_ASSIGN( + auto result, DestListArrayClass::FromListView( + checked_cast(sliced), pool_)); + ASSERT_OK(result->ValidateFull()); + AssertArraysEqual(*expected, *result, /*verbose=*/true); + }; + + // A slice whose element offset is not a multiple of 8, so the bitmap's bit + // offset and byte offset differ (GH-51613). + check(*list_view->Slice(1, 6), + ArrayFromJSON(list_type, "[[3], null, [], [4], [5, 6], null]")); + // A slice starting on a byte boundary, so that the bit offset is zero. + check(*list_view->Slice(2, 4), ArrayFromJSON(list_type, "[null, [], [4], [5, 6]]")); + // A slice whose only null is the last slot. + check(*list_view->Slice(0, 8), + ArrayFromJSON(list_type, "[[1, 2], [3], null, [], [4], [5, 6], null, [7]]")); + } }; TEST_F(TestListConversions, ListViewFromList) { @@ -1061,6 +1090,11 @@ TEST_F(TestListConversions, ListFromListView) { this->DoTestListFromListView(); } +TEST_F(TestListConversions, ListFromSlicedListView) { + this->DoTestListFromSlicedListView(); + this->DoTestListFromSlicedListView(); +} + // ---------------------------------------------------------------------- // Map tests diff --git a/cpp/src/arrow/array/array_nested.cc b/cpp/src/arrow/array/array_nested.cc index 6849aca089b7..55dd77267a60 100644 --- a/cpp/src/arrow/array/array_nested.cc +++ b/cpp/src/arrow/array/array_nested.cc @@ -415,37 +415,8 @@ Result> ListFromListViewImpl( std::is_same::value, "Offset types between list type and list-view type are expected to match"); - using offset_type = typename DestListType::offset_type; - using ListBuilderType = typename TypeTraits::BuilderType; - - const auto& list_view_type = - checked_cast(*list_view_data->type); - const auto& value_type = list_view_type.value_type(); - const auto list_type = std::make_shared(value_type); - - ARROW_ASSIGN_OR_RAISE(auto sum_of_list_view_sizes, - list_util::internal::SumOfLogicalListSizes(*list_view_data)); - ARROW_ASSIGN_OR_RAISE(std::shared_ptr value_builder, - MakeBuilder(value_type, pool)); - RETURN_NOT_OK(value_builder->Reserve(sum_of_list_view_sizes)); - auto list_builder = std::make_shared(pool, value_builder, list_type); - RETURN_NOT_OK(list_builder->Reserve(list_view_data->length)); - - ArraySpan values{*list_view_data->child_data[0]}; - const auto* in_validity_bitmap = list_view_data->GetValues(0); - const auto* in_offsets = list_view_data->GetValues(1); - const auto* in_sizes = list_view_data->GetValues(2); - for (int64_t i = 0; i < list_view_data->length; ++i) { - const bool is_valid = - !in_validity_bitmap || - bit_util::GetBit(in_validity_bitmap, list_view_data->offset + i); - const int64_t size = is_valid ? in_sizes[i] : 0; - RETURN_NOT_OK(list_builder->Append(is_valid, size)); - RETURN_NOT_OK(value_builder->AppendArraySlice(values, in_offsets[i], size)); - } - std::shared_ptr list_array_data; - RETURN_NOT_OK(list_builder->FinishInternal(&list_array_data)); - return list_array_data; + return list_util::internal::ListFromListView(ArraySpan{*list_view_data}, + DestListType::type_id, pool); } } // namespace diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc b/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc index 392fd9fbb705..3729d7e372db 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc @@ -30,6 +30,7 @@ #include "arrow/compute/kernels/scalar_cast_internal.h" #include "arrow/util/bitmap_ops.h" #include "arrow/util/int_util.h" +#include "arrow/util/list_util.h" #include "arrow/util/logging_internal.h" namespace arrow { @@ -105,7 +106,37 @@ struct CastList { return Status::OK(); } + /// \brief Cast a list-view array to a (large)list array + /// + /// The values referenced by a list-view have to be gathered according to the + /// views' offsets and sizes, so the views' offsets buffer cannot be reused as + /// a list-offsets buffer (GH-51612). + static Status ExecListView(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { + const CastOptions& options = CastState::Get(ctx); + auto child_type = checked_cast(*out->type()).value_type(); + + ARROW_ASSIGN_OR_RAISE(std::shared_ptr list_data, + list_util::internal::ListFromListView( + batch[0].array, DestType::type_id, ctx->memory_pool())); + + ARROW_ASSIGN_OR_RAISE(Datum cast_values, Cast(list_data->child_data[0], child_type, + options, ctx->exec_context())); + DCHECK(cast_values.is_array()); + + ArrayData* out_array = out->array_data().get(); + out_array->buffers[0] = std::move(list_data->buffers[0]); + out_array->buffers[1] = std::move(list_data->buffers[1]); + out_array->null_count.store(list_data->null_count.load()); + out_array->offset = list_data->offset; + out_array->child_data.push_back(cast_values.array()); + return Status::OK(); + } + static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { + if constexpr (is_list_view(SrcType::type_id)) { + return ExecListView(ctx, batch, out); + } + const CastOptions& options = CastState::Get(ctx); auto child_type = checked_cast(*out->type()).value_type(); diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc index 864ec4afce0c..f887fc402064 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc @@ -3616,6 +3616,135 @@ static void CheckListToList(const std::vector>& value_ } } +TEST(Cast, ListViewToList) { + // Views may be out of order or overlapping, so the referenced values have to + // be gathered rather than reinterpreted (GH-51612). + auto values = ArrayFromJSON(int32(), "[1, 2, 3, 4]"); + + // Out of order and overlapping views. + auto offsets = ArrayFromJSON(int32(), "[2, 0, 1]"); + auto sizes = ArrayFromJSON(int32(), "[2, 2, 1]"); + ASSERT_OK_AND_ASSIGN(auto list_view, + ListViewArray::FromArrays(*offsets, *sizes, *values)); + CheckCast(list_view, ArrayFromJSON(list(int32()), "[[3, 4], [1, 2], [2]]")); + // Same offset width, larger offset width, and a child type change. + CheckCast(list_view, ArrayFromJSON(large_list(int32()), "[[3, 4], [1, 2], [2]]")); + CheckCast(list_view, ArrayFromJSON(list(int64()), "[[3, 4], [1, 2], [2]]")); + CheckCast(list_view, ArrayFromJSON(large_list(float32()), "[[3, 4], [1, 2], [2]]")); + + // The large_list_view source, narrowing as well as widening the offset width. + ASSERT_OK_AND_ASSIGN( + auto large_list_view, + LargeListViewArray::FromArrays(*ArrayFromJSON(int64(), "[2, 0, 1]"), + *ArrayFromJSON(int64(), "[2, 2, 1]"), *values)); + CheckCast(large_list_view, ArrayFromJSON(large_list(int32()), "[[3, 4], [1, 2], [2]]")); + CheckCast(large_list_view, ArrayFromJSON(list(int32()), "[[3, 4], [1, 2], [2]]")); + + // Views that happen to be contiguous, i.e. what a list array would look like. + ASSERT_OK_AND_ASSIGN(list_view, ListViewArray::FromArrays( + *ArrayFromJSON(int32(), "[0, 2, 4]"), + *ArrayFromJSON(int32(), "[2, 2, 0]"), *values)); + CheckCast(list_view, ArrayFromJSON(list(int32()), "[[1, 2], [3, 4], []]")); + + // Nulls. + { + auto list_view_w_nulls = MaskArrayWithNullsAt(list_view, {0}); + CheckCast(list_view_w_nulls, ArrayFromJSON(list(int32()), "[null, [3, 4], []]")); + } + { + // A null view contributes no values, whatever size it declares. + ASSERT_OK_AND_ASSIGN(auto lv, ListViewArray::FromArrays( + *ArrayFromJSON(int32(), "[0, 2, 2]"), + *ArrayFromJSON(int32(), "[2, 2, 2]"), *values)); + CheckCast(MaskArrayWithNullsAt(lv, {2}), + ArrayFromJSON(list(int32()), "[[1, 2], [3, 4], null]")); + } + + // Sliced list-view. + { + ASSERT_OK_AND_ASSIGN( + auto lv, ListViewArray::FromArrays(*ArrayFromJSON(int32(), "[0, 2, 0, 1, 2, 0]"), + *ArrayFromJSON(int32(), "[2, 2, 2, 0, 2, 2]"), + *values)); + CheckCast(lv->Slice(2, 3), ArrayFromJSON(list(int32()), "[[1, 2], [], [3, 4]]")); + CheckCast(lv->Slice(1, 4), + ArrayFromJSON(large_list(int32()), "[[3, 4], [1, 2], [], [3, 4]]")); + } + + // Options pass through to the child cast. + { + ASSERT_OK_AND_ASSIGN( + auto lv, ListViewArray::FromArrays( + *ArrayFromJSON(int32(), "[0, 2]"), *ArrayFromJSON(int32(), "[2, 2]"), + *ArrayFromJSON(int64(), "[87654321, 0, 0, 0]"))); + auto options = CastOptions::Safe(list(int16())); + CheckCastFails(lv, options); + options.allow_int_overflow = true; + CheckCast(lv, ArrayFromJSON(list(int16()), "[[32689, 0], [0, 0]]"), options); + } +} + +TEST(Cast, ListViewToListEdgeCases) { + auto values = ArrayFromJSON(int32(), "[1, 2, 3, 4]"); + + auto make = [](const char* offsets, const char* sizes, + const std::shared_ptr& values) { + return ListViewArray::FromArrays(*ArrayFromJSON(int32(), offsets), + *ArrayFromJSON(int32(), sizes), *values); + }; + + // Zero-length list-view. + CheckCast(ArrayFromJSON(list_view(int32()), "[]"), ArrayFromJSON(list(int32()), "[]")); + CheckCast(ArrayFromJSON(large_list_view(int32()), "[]"), + ArrayFromJSON(large_list(int32()), "[]")); + + // All views null, several with a non-zero declared size: no value is gathered. + { + ASSERT_OK_AND_ASSIGN(auto lv, make("[0, 2, 4]", "[2, 2, 2]", values)); + CheckCast(MaskArrayWithNullsAt(lv, {0, 1, 2}), + ArrayFromJSON(list(int32()), "[null, null, null]")); + } + + // Out-of-order/overlapping views combined with nulls. + { + ASSERT_OK_AND_ASSIGN(auto lv, make("[2, 0, 1]", "[2, 2, 1]", values)); + CheckCast(MaskArrayWithNullsAt(lv, {1}), + ArrayFromJSON(list(int32()), "[[3, 4], null, [2]]")); + } + + // A view of size zero interleaved with out-of-order views. + { + ASSERT_OK_AND_ASSIGN(auto lv, make("[1, 3, 0, 2]", "[0, 1, 4, 2]", values)); + CheckCast(lv, ArrayFromJSON(list(int32()), "[[], [4], [1, 2, 3, 4], [3, 4]]")); + } + + // The list-view's child array is itself sliced, i.e. has a non-zero offset. + { + auto sliced_values = ArrayFromJSON(int32(), "[9, 1, 2, 3, 4, 9]")->Slice(1, 4); + ASSERT_OK_AND_ASSIGN(auto lv, make("[2, 0, 1]", "[2, 2, 1]", sliced_values)); + CheckCast(lv, ArrayFromJSON(list(int32()), "[[3, 4], [1, 2], [2]]")); + } + + // Nested list-view, exercising the gather on a non-primitive child. + { + auto nested_values = + ArrayFromJSON(list(int32()), "[[10], [20, 21], [30, 31, 32], [40]]"); + ASSERT_OK_AND_ASSIGN(auto lv, make("[1, 0, 2]", "[1, 2, 2]", nested_values)); + CheckCast(lv, ArrayFromJSON(large_list(list(int64())), + "[[[20, 21]], [[10], [20, 21]], [[30, 31, 32], [40]]]")); + } + + // A sliced list-view whose validity bitmap bit offset is non-zero. + { + auto lv = ArrayFromJSON(list_view(int32()), + "[[1, 2], [3], null, [], [4], [5, 6], null, [7], [8, 9]]"); + CheckCast(lv->Slice(1, 7), + ArrayFromJSON(list(int32()), "[[3], null, [], [4], [5, 6], null, [7]]")); + CheckCast(lv->Slice(1, 7), ArrayFromJSON(large_list(int64()), + "[[3], null, [], [4], [5, 6], null, [7]]")); + } +} + TEST(Cast, ListToList) { CheckListToList({int32(), float32(), int64()}, "[[0], [1], null, [2, 3, 4], [5, 6], null, [], [7], [8, 9]]"); diff --git a/cpp/src/arrow/util/list_util.cc b/cpp/src/arrow/util/list_util.cc index 8d487252b9e3..2c8b52fc7a08 100644 --- a/cpp/src/arrow/util/list_util.cc +++ b/cpp/src/arrow/util/list_util.cc @@ -16,9 +16,11 @@ // under the License. #include +#include #include #include "arrow/array/array_nested.h" +#include "arrow/array/builder_base.h" #include "arrow/array/builder_nested.h" #include "arrow/array/data.h" #include "arrow/type.h" @@ -190,8 +192,78 @@ int64_t SumOfListViewSizes(const ArraySpan& input) { return sum; } +/// \param list_view A LIST_VIEW or LARGE_LIST_VIEW array +template +Result> ListFromListViewImpl(const ArraySpan& list_view, + MemoryPool* pool) { + using src_offset_type = typename SrcListViewType::offset_type; + using ListBuilderType = typename TypeTraits::BuilderType; + + const auto& list_view_type = checked_cast(*list_view.type); + const auto& value_type = list_view_type.value_type(); + // The value type is carried over, so that callers may cast the child. + auto list_type = std::make_shared(value_type); + + ARROW_ASSIGN_OR_RAISE(auto sum_of_sizes, SumOfLogicalListSizes(list_view)); + ARROW_ASSIGN_OR_RAISE(std::shared_ptr value_builder, + MakeBuilder(value_type, pool)); + RETURN_NOT_OK(value_builder->Reserve(sum_of_sizes)); + auto list_builder = std::make_shared(pool, value_builder, list_type); + RETURN_NOT_OK(list_builder->Reserve(list_view.length)); + + // The bitmap is bit-packed, so it must be read with the element offset + // applied as a bit offset, not as the byte offset GetValues() would apply. + const uint8_t* validity = list_view.buffers[0].data; + const auto* offsets = list_view.GetValues(1); + const auto* sizes = list_view.GetValues(2); + const ArraySpan values{list_view.child_data[0]}; + for (int64_t i = 0; i < list_view.length; ++i) { + const bool is_valid = !validity || bit_util::GetBit(validity, list_view.offset + i); + // A null view contributes no values, whatever size it declares. + const int64_t size = is_valid ? sizes[i] : 0; + RETURN_NOT_OK(list_builder->Append(is_valid, size)); + RETURN_NOT_OK(value_builder->AppendArraySlice(values, offsets[i], size)); + } + + std::shared_ptr list_data; + RETURN_NOT_OK(list_builder->FinishInternal(&list_data)); + return list_data; +} + } // namespace +Result> ListFromListView(const ArraySpan& input, + Type::type dest_type_id, + MemoryPool* pool) { + switch (input.type->id()) { + case Type::LIST_VIEW: + switch (dest_type_id) { + case Type::LIST: + return ListFromListViewImpl(input, pool); + case Type::LARGE_LIST: + return ListFromListViewImpl(input, pool); + default: + break; + } + break; + case Type::LARGE_LIST_VIEW: + switch (dest_type_id) { + case Type::LIST: + return ListFromListViewImpl(input, pool); + case Type::LARGE_LIST: + return ListFromListViewImpl(input, pool); + default: + break; + } + break; + default: + break; + } + return Status::TypeError("ListFromListView: expected a list-view array and a ", + arrow::internal::ToString(dest_type_id), + " destination type, got ", input.type->ToString()); +} + Result> RangeOfValuesUsed(const ArraySpan& input) { switch (input.type->id()) { case Type::LIST: diff --git a/cpp/src/arrow/util/list_util.h b/cpp/src/arrow/util/list_util.h index 58deb8019d94..599ff8a7b4ae 100644 --- a/cpp/src/arrow/util/list_util.h +++ b/cpp/src/arrow/util/list_util.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include "arrow/array/data.h" @@ -49,6 +50,20 @@ ARROW_EXPORT Result> RangeOfValuesUsed( /// \return The sum of all list or list-view sizes ARROW_EXPORT Result SumOfLogicalListSizes(const ArraySpan& input); +/// \brief Materialize a (large)list array from a list-view array +/// +/// The returned array data has `dest_type_id`'s layout: freshly computed list +/// offsets and a child array gathering the values referenced by every view, in +/// list order. The value type is the input's, so that callers may cast the +/// child afterwards. +/// +/// \param input The input list-view array +/// \param dest_type_id Either `Type::LIST` or `Type::LARGE_LIST` +/// \param pool MemoryPool used for the allocations +ARROW_EXPORT Result> ListFromListView(const ArraySpan& input, + Type::type dest_type_id, + MemoryPool* pool); + } // namespace internal } // namespace list_util diff --git a/cpp/src/arrow/util/list_util_test.cc b/cpp/src/arrow/util/list_util_test.cc index 4021180b2bef..5e841db7c083 100644 --- a/cpp/src/arrow/util/list_util_test.cc +++ b/cpp/src/arrow/util/list_util_test.cc @@ -17,7 +17,9 @@ #include +#include "arrow/array/array_nested.h" #include "arrow/array/builder_nested.h" +#include "arrow/array/util.h" #include "arrow/util/list_util.h" #include "arrow/testing/builder.h" @@ -160,4 +162,57 @@ TYPED_TEST_SUITE(TestListUtils, ListAndListViewTypes); TYPED_TEST(TestListUtils, RangeOfValuesUsed) { this->TestRangeOfValuesUsed(); } +TEST(TestListFromListView, GathersOutOfOrderViews) { + // The behavioural contract is covered by Cast.ListViewToList; the null-view + // and error cases are pinned here where the helper lives. + ASSERT_OK_AND_ASSIGN( + auto view, ListViewArray::FromArrays(*ArrayFromJSON(int32(), "[2, 0, 1]"), + *ArrayFromJSON(int32(), "[2, 2, 1]"), + *ArrayFromJSON(int32(), "[1, 2, 3, 4]"))); + ASSERT_OK_AND_ASSIGN(auto data, list_util::internal::ListFromListView( + *view->data(), Type::LIST, default_memory_pool())); + auto result = MakeArray(data); + ASSERT_OK(result->ValidateFull()); + AssertArraysEqual(*ArrayFromJSON(list(int32()), "[[3, 4], [1, 2], [2]]"), *result, + /*verbose=*/true); +} + +TEST(TestListFromListView, SkipsValuesOfNullViews) { + // A null view must not contribute any values, whatever size it declares. + ASSERT_OK_AND_ASSIGN(auto validity, AllocateEmptyBitmap(3)); + // A set bit means valid, so start from "all valid" then clear the middle one. + bit_util::SetBitTo(validity->mutable_data(), 0, /*value=*/true); + bit_util::SetBitTo(validity->mutable_data(), 1, /*value=*/false); + bit_util::SetBitTo(validity->mutable_data(), 2, /*value=*/true); + ASSERT_OK_AND_ASSIGN(auto view, + ListViewArray::FromArrays(*ArrayFromJSON(int32(), "[0, 2, 2]"), + *ArrayFromJSON(int32(), "[2, 2, 2]"), + *ArrayFromJSON(int32(), "[1, 2, 3, 4]"), + default_memory_pool(), validity, + /*null_count=*/1)); + ASSERT_OK_AND_ASSIGN(auto data, list_util::internal::ListFromListView( + *view->data(), Type::LIST, default_memory_pool())); + auto result = MakeArray(data); + ASSERT_OK(result->ValidateFull()); + AssertArraysEqual(*ArrayFromJSON(list(int32()), "[[1, 2], null, [3, 4]]"), *result, + /*verbose=*/true); +} + +TEST(TestListFromListView, RejectsNonListViewInput) { + // Only list-views can be converted: a plain list has no independent sizes. + auto array = ArrayFromJSON(list(int32()), "[[1, 2], [3]]"); + ASSERT_RAISES(TypeError, list_util::internal::ListFromListView( + *array->data(), Type::LIST, default_memory_pool())); +} + +TEST(TestListFromListView, RejectsNonListDestinationType) { + ASSERT_OK_AND_ASSIGN( + auto view, ListViewArray::FromArrays(*ArrayFromJSON(int32(), "[0, 2]"), + *ArrayFromJSON(int32(), "[2, 2]"), + *ArrayFromJSON(int32(), "[1, 2, 3, 4]"))); + ASSERT_RAISES(TypeError, + list_util::internal::ListFromListView( + *view->data(), Type::FIXED_SIZE_LIST, default_memory_pool())); +} + } // namespace arrow diff --git a/python/pyarrow/tests/test_array.py b/python/pyarrow/tests/test_array.py index eb81a4409e8d..eaffbdd52056 100644 --- a/python/pyarrow/tests/test_array.py +++ b/python/pyarrow/tests/test_array.py @@ -4453,6 +4453,60 @@ def test_list_view_slice(list_view_type): assert sliced_array[0].as_py() == sliced_array.values[i:j].to_pylist() == [4] +@pytest.mark.parametrize( + ('list_view_array_type', 'offset_type', 'list_factory'), + [ + (pa.ListViewArray, pa.int32(), pa.list_), + (pa.ListViewArray, pa.int32(), pa.large_list), + (pa.LargeListViewArray, pa.int64(), pa.list_), + (pa.LargeListViewArray, pa.int64(), pa.large_list), + ]) +def test_cast_list_view_to_list(list_view_array_type, offset_type, list_factory): + # Casting a list-view to a list has to gather the values referenced by each + # view: views are neither required to be laid out in order nor to be + # non-overlapping. See GH-51612. + views = list_view_array_type.from_arrays( + pa.array([2, 0, 1], type=offset_type), # offsets + pa.array([2, 2, 1], type=offset_type), # sizes + pa.array([1, 2, 3, 4], type=pa.int32()), # values + ) + + assert views.to_pylist() == [[3, 4], [1, 2], [2]] + + # The child values are cast as well. + for value_type, expected in [ + (pa.int32(), [[3, 4], [1, 2], [2]]), + (pa.int64(), [[3, 4], [1, 2], [2]]), + (pa.float64(), [[3.0, 4.0], [1.0, 2.0], [2.0]])]: + assert views.cast(list_factory(value_type)).to_pylist() == expected + + +def test_cast_list_view_to_list_nulls(): + # A null view must not contribute any values, whatever size it declares. + views = pa.ListViewArray.from_arrays( + pa.array([0, 2, 2], type=pa.int32()), # offsets + pa.array([2, 2, 2], type=pa.int32()), # sizes + pa.array([1, 2, 3, 4], type=pa.int32()), # values + mask=pa.array([False, True, False]), # True means null + ) + + assert views.to_pylist() == [[1, 2], None, [3, 4]] + assert views.cast(pa.list_(pa.int32())).to_pylist() == [[1, 2], None, [3, 4]] + + +def test_cast_list_view_to_list_sliced(): + # The validity bitmap of a sliced list-view must be read with the element + # offset applied as a bit offset. See GH-51613. + views = pa.array( + [[1, 2], [3], None, [], [4], [5, 6], None, [7], [8, 9]], + type=pa.list_view(pa.int32()), + ) + + sliced = views.slice(1, 7) + assert sliced.cast(pa.list_(pa.int32())).to_pylist() == [ + [3], None, [], [4], [5, 6], None, [7]] + + @pytest.mark.numpy @pytest.mark.parametrize('numpy_native_dtype', ['u2', 'i4', 'f8']) def test_swapped_byte_order_fails(numpy_native_dtype):