Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions cpp/src/arrow/array/array_list_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,35 @@ class TestListConversions : public ::testing::Test {
ASSERT_OK(result->ValidateFull());
AssertArraysEqual(*expected_list_wo_nulls, *result, /*verbose=*/true);
}

template <typename DestListType, typename SrcListViewType>
void DoTestListFromSlicedListView() {
using SrcListViewArrayClass = typename TypeTraits<SrcListViewType>::ArrayType;
using DestListArrayClass = typename TypeTraits<DestListType>::ArrayType;
auto list_view_type = std::make_shared<SrcListViewType>(int32());
auto list_type = std::make_shared<DestListType>(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<Array>& expected) {
ASSERT_OK_AND_ASSIGN(
auto result, DestListArrayClass::FromListView(
checked_cast<const SrcListViewArrayClass&>(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) {
Expand All @@ -1061,6 +1090,11 @@ TEST_F(TestListConversions, ListFromListView) {
this->DoTestListFromListView<LargeListType, LargeListViewType>();
}

TEST_F(TestListConversions, ListFromSlicedListView) {
this->DoTestListFromSlicedListView<ListType, ListViewType>();
this->DoTestListFromSlicedListView<LargeListType, LargeListViewType>();
}

// ----------------------------------------------------------------------
// Map tests

Expand Down
33 changes: 2 additions & 31 deletions cpp/src/arrow/array/array_nested.cc
Original file line number Diff line number Diff line change
Expand Up @@ -415,37 +415,8 @@ Result<std::shared_ptr<ArrayData>> ListFromListViewImpl(
std::is_same<typename SrcListViewType::offset_type,
typename DestListType::offset_type>::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<DestListType>::BuilderType;

const auto& list_view_type =
checked_cast<const SrcListViewType&>(*list_view_data->type);
const auto& value_type = list_view_type.value_type();
const auto list_type = std::make_shared<DestListType>(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<ArrayBuilder> value_builder,
MakeBuilder(value_type, pool));
RETURN_NOT_OK(value_builder->Reserve(sum_of_list_view_sizes));
auto list_builder = std::make_shared<ListBuilderType>(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<uint8_t>(0);
const auto* in_offsets = list_view_data->GetValues<offset_type>(1);
const auto* in_sizes = list_view_data->GetValues<offset_type>(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<ArrayData> 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
Expand Down
31 changes: 31 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_cast_nested.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<const DestType&>(*out->type()).value_type();

ARROW_ASSIGN_OR_RAISE(std::shared_ptr<ArrayData> 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<const DestType&>(*out->type()).value_type();
Expand Down
129 changes: 129 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_cast_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3616,6 +3616,135 @@ static void CheckListToList(const std::vector<std::shared_ptr<DataType>>& 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<Array>& 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]]");
Expand Down
72 changes: 72 additions & 0 deletions cpp/src/arrow/util/list_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
// under the License.

#include <cstdint>
#include <memory>
#include <vector>

#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"
Expand Down Expand Up @@ -190,8 +192,78 @@ int64_t SumOfListViewSizes(const ArraySpan& input) {
return sum;
}

/// \param list_view A LIST_VIEW or LARGE_LIST_VIEW array
template <typename DestListType, typename SrcListViewType>
Result<std::shared_ptr<ArrayData>> ListFromListViewImpl(const ArraySpan& list_view,
MemoryPool* pool) {
using src_offset_type = typename SrcListViewType::offset_type;
using ListBuilderType = typename TypeTraits<DestListType>::BuilderType;

const auto& list_view_type = checked_cast<const SrcListViewType&>(*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<DestListType>(value_type);

ARROW_ASSIGN_OR_RAISE(auto sum_of_sizes, SumOfLogicalListSizes(list_view));
ARROW_ASSIGN_OR_RAISE(std::shared_ptr<ArrayBuilder> value_builder,
MakeBuilder(value_type, pool));
RETURN_NOT_OK(value_builder->Reserve(sum_of_sizes));
auto list_builder = std::make_shared<ListBuilderType>(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<src_offset_type>(1);
const auto* sizes = list_view.GetValues<src_offset_type>(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<ArrayData> list_data;
RETURN_NOT_OK(list_builder->FinishInternal(&list_data));
return list_data;
}

} // namespace

Result<std::shared_ptr<ArrayData>> 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<ListType, ListViewType>(input, pool);
case Type::LARGE_LIST:
return ListFromListViewImpl<LargeListType, ListViewType>(input, pool);
default:
break;
}
break;
case Type::LARGE_LIST_VIEW:
switch (dest_type_id) {
case Type::LIST:
return ListFromListViewImpl<ListType, LargeListViewType>(input, pool);
case Type::LARGE_LIST:
return ListFromListViewImpl<LargeListType, LargeListViewType>(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<std::pair<int64_t, int64_t>> RangeOfValuesUsed(const ArraySpan& input) {
switch (input.type->id()) {
case Type::LIST:
Expand Down
15 changes: 15 additions & 0 deletions cpp/src/arrow/util/list_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#pragma once

#include <cstdint>
#include <memory>
#include <utility>

#include "arrow/array/data.h"
Expand Down Expand Up @@ -49,6 +50,20 @@ ARROW_EXPORT Result<std::pair<int64_t, int64_t>> RangeOfValuesUsed(
/// \return The sum of all list or list-view sizes
ARROW_EXPORT Result<int64_t> 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<std::shared_ptr<ArrayData>> ListFromListView(const ArraySpan& input,
Type::type dest_type_id,
MemoryPool* pool);

} // namespace internal

} // namespace list_util
Expand Down
Loading
Loading