diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc b/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc index 392fd9fbb70..653c33bcb10 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_nested.cc @@ -236,9 +236,9 @@ struct CastVarToFixedList { RETURN_NOT_OK(builder.Reserve(in_array.length * list_size)); for (int64_t offset_i = 0; offset_i < in_array.length; ++offset_i) { if (in_array.IsNull(offset_i)) { - // If element is null, just fill in the null slots with first value. + // If element is null, fill in the null slots with null values. for (int64_t j = 0; j < list_size; ++j) { - builder.UnsafeAppend(0); + builder.UnsafeAppendNull(); } } else { int64_t value_i = offsets[offset_i]; @@ -275,18 +275,25 @@ struct CastVarToFixedList { DCHECK(cast_values_datum.is_array()); std::shared_ptr cast_values = cast_values_datum.array(); - const auto* offsets = in_array.GetValues(1); - if (in_array.GetNullCount() > 0 && !nulls_have_correct_length) { - // We need to fill in the null slots, so we'll use Take on the values. - ARROW_ASSIGN_OR_RAISE(auto indices, BuildTakeIndices(in_array, list_size, ctx)); - ARROW_ASSIGN_OR_RAISE(auto take_result, - Take(cast_values, Datum(indices), - TakeOptions::NoBoundsCheck(), ctx->exec_context())); - DCHECK(take_result.is_array()); - cast_values = take_result.array(); - } else if (offsets[0] != 0) { - // No nulls, but we need to slice the values - cast_values = cast_values->Slice(offsets[0], in_array.length * list_size); + if (cast_values->length == 0) { + ARROW_ASSIGN_OR_RAISE( + auto null_values, + MakeArrayOfNull(child_type, in_array.length * list_size, ctx->memory_pool())); + cast_values = null_values->data(); + } else { + const auto* offsets = in_array.GetValues(1); + if (in_array.GetNullCount() > 0 && !nulls_have_correct_length) { + // We need to fill in the null slots, so we'll use Take on the values. + ARROW_ASSIGN_OR_RAISE(auto indices, BuildTakeIndices(in_array, list_size, ctx)); + ARROW_ASSIGN_OR_RAISE(auto take_result, + Take(cast_values, Datum(indices), + TakeOptions::NoBoundsCheck(), ctx->exec_context())); + DCHECK(take_result.is_array()); + cast_values = take_result.array(); + } else if (offsets[0] != 0 || cast_values->length > in_array.length * list_size) { + // No nulls or nulls have correct length, but we need to slice the values + cast_values = cast_values->Slice(offsets[0], in_array.length * list_size); + } } out_array->child_data.emplace_back(std::move(cast_values)); diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc index 864ec4afce0..a232511c2c7 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc @@ -3787,6 +3787,72 @@ TEST(Cast, ListToFSL) { // Invalid fixed_size_list cast if inconsistent size ASSERT_RAISES(Invalid, Cast(ArrayFromJSON(list(int32()), "[[0, 1, 2], null, [3, 4]]"), CastOptions::Safe(fixed_size_list(int32(), 3)))); + + // GH-54570: Null list array with empty child array cast to FixedSizeList + { + auto child = ArrayFromJSON(int64(), "[]"); + ASSERT_OK_AND_ASSIGN( + auto from, + ListArray::FromArrays(*ArrayFromJSON(int32(), "[0, 0]"), *child, + *ArrayFromJSON(boolean(), "[false]"))); + auto to = ArrayFromJSON(fixed_size_list(int64(), 3), "[null]"); + CheckCast(from, to); + } + + // GH-54570: LargeList with empty child cast to FixedSizeList + { + auto child = ArrayFromJSON(int64(), "[]"); + ASSERT_OK_AND_ASSIGN( + auto from, + LargeListArray::FromArrays(*ArrayFromJSON(int64(), "[0, 0]"), *child, + *ArrayFromJSON(boolean(), "[false]"))); + auto to = ArrayFromJSON(fixed_size_list(int64(), 3), "[null]"); + CheckCast(from, to); + } + + // GH-54570: Multiple null lists with empty child + { + auto child = ArrayFromJSON(int64(), "[]"); + ASSERT_OK_AND_ASSIGN( + auto from, + ListArray::FromArrays(*ArrayFromJSON(int32(), "[0, 0, 0]"), *child, + *ArrayFromJSON(boolean(), "[false, false]"))); + auto to = ArrayFromJSON(fixed_size_list(int64(), 2), "[null, null]"); + CheckCast(from, to); + } + + // GH-54570: Null list with unallocated/null child data buffers + { + auto child_data = ArrayData::Make(int64(), 0, {nullptr, nullptr}); + auto list_bitmap = ArrayFromJSON(boolean(), "[false]")->data()->buffers[1]; + auto list_offsets = ArrayFromJSON(int32(), "[0, 0]")->data()->buffers[1]; + auto list_data = ArrayData::Make(list(int64()), 1, {list_bitmap, list_offsets}, + {child_data}, 1); + auto from = MakeArray(list_data); + auto to = ArrayFromJSON(fixed_size_list(int64(), 3), "[null]"); + CheckCast(from, to); + } + + // GH-54570: Null slots with length 0 alongside valid slots + { + auto child = ArrayFromJSON(int32(), "[10, 20]"); + ASSERT_OK_AND_ASSIGN( + auto from, + ListArray::FromArrays(*ArrayFromJSON(int32(), "[0, 0, 2]"), *child, + *ArrayFromJSON(boolean(), "[false, true]"))); + auto to = ArrayFromJSON(fixed_size_list(int32(), 2), "[null, [10, 20]]"); + CheckCast(from, to); + } + + { + auto child = ArrayFromJSON(int32(), "[10, 20]"); + ASSERT_OK_AND_ASSIGN( + auto from, + ListArray::FromArrays(*ArrayFromJSON(int32(), "[0, 2, 2]"), *child, + *ArrayFromJSON(boolean(), "[true, false]"))); + auto to = ArrayFromJSON(fixed_size_list(int32(), 2), "[[10, 20], null]"); + CheckCast(from, to); + } } TEST(Cast, CastMap) {