From 942d0012453153b283a10ca695f9986b75ce4737 Mon Sep 17 00:00:00 2001 From: Kartik00052 Date: Tue, 29 Sep 2026 23:03:23 +0530 Subject: [PATCH] GH-51639: [C++] Fix invalid result when casting struct with null-typed child to its own type --- .../compute/kernels/scalar_cast_internal.cc | 5 ++-- .../arrow/compute/kernels/scalar_cast_test.cc | 28 +++++++++++++++++++ python/pyarrow/tests/parquet/test_basic.py | 17 +++++++++++ python/pyarrow/tests/test_compute.py | 28 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_internal.cc b/cpp/src/arrow/compute/kernels/scalar_cast_internal.cc index 0484e0b259c5..2731a7fcf6b5 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_internal.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_internal.cc @@ -214,7 +214,7 @@ Status OutputAllNull(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) // should clean this up later. This is used in the dict->null cast ArrayData* output = out->array_data().get(); output->buffers = {nullptr}; - output->null_count = batch.length; + output->null_count = batch[0].array.length; return Status::OK(); } @@ -234,7 +234,8 @@ Status CastFromExtension(KernelContext* ctx, const ExecSpan& batch, ExecResult* Status CastFromNull(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { // TODO(wesm): handle this case more gracefully std::shared_ptr nulls; - RETURN_NOT_OK(MakeArrayOfNull(out->type()->GetSharedPtr(), batch.length).Value(&nulls)); + RETURN_NOT_OK( + MakeArrayOfNull(out->type()->GetSharedPtr(), batch[0].array.length).Value(&nulls)); out->value = nulls->data(); return Status::OK(); } diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc index 864ec4afce0c..ce9a73119d30 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc @@ -4309,6 +4309,34 @@ TEST(Cast, IdentityCasts) { CheckIdentityCast(dictionary(int8(), int8()), "[1, 2, 3, 1, null, 3]"); } +TEST(Cast, NullChildLength) { + const auto struct_type = arrow::struct_({std::make_shared("a", int64(), false), + std::make_shared("n", null())}); + + auto struct_array = ArrayFromJSON( + struct_type, R"([{"a": 1, "n": null}, {"a": 2, "n": null}])"); + ASSERT_OK_AND_ASSIGN(auto sliced_struct, Cast(*struct_array->Slice(1, 1), struct_type)); + ASSERT_OK(sliced_struct->ValidateFull()); + + const auto output_list_type = list(struct_type); + for (const auto& source_list_type : {list(struct_type), large_list(struct_type), + list_view(struct_type), large_list_view(struct_type)}) { + auto list_array = ArrayFromJSON( + source_list_type, R"([[{"a": 1, "n": null}, {"a": 2, "n": null}]])"); + ASSERT_OK_AND_ASSIGN(auto cast, Cast(*list_array, output_list_type)); + ASSERT_OK(cast->ValidateFull()); + } + + const auto nested_type = arrow::struct_({ + std::make_shared("a", arrow::struct_({std::make_shared("n", null())}), + false), + std::make_shared("b", int64(), false)}); + auto nested_array = + ArrayFromJSON(nested_type, R"([{"a": {"n": null}, "b": 1}, null])"); + ASSERT_OK_AND_ASSIGN(auto nested_cast, Cast(*nested_array, nested_type)); + ASSERT_OK(nested_cast->ValidateFull()); +} + TEST(Cast, EmptyCasts) { // ARROW-4766: 0-length arrays should not segfault auto CheckCastEmpty = [](std::shared_ptr from, std::shared_ptr to) { diff --git a/python/pyarrow/tests/parquet/test_basic.py b/python/pyarrow/tests/parquet/test_basic.py index 8b91090989ab..9084eea24dd0 100644 --- a/python/pyarrow/tests/parquet/test_basic.py +++ b/python/pyarrow/tests/parquet/test_basic.py @@ -68,6 +68,23 @@ def test_parquet_invalid_version(tempdir): data_page_version="2.2") +def test_cast_struct_with_null_child_from_parquet(tempdir): + struct_type = pa.struct([ + pa.field('a', pa.int64(), nullable=False), + pa.field('n', pa.null()), + ]) + array = pa.array([None, [{'a': 1, 'n': None}, {'a': 2, 'n': None}]], + type=pa.list_(struct_type)) + path = tempdir / 'struct_null_child.parquet' + _write_table(pa.table({'x': array}), path, row_group_size=1) + + schema = pq.read_schema(path) + parquet_file = pq.ParquetFile(path) + for batch in parquet_file.iter_batches(batch_size=1): + casted = batch.cast(schema) + assert casted.column(0).validate(full=True) is None + + def test_set_data_page_size(): arr = pa.array([1, 2, 3] * 100000) t = pa.Table.from_arrays([arr], names=['f0']) diff --git a/python/pyarrow/tests/test_compute.py b/python/pyarrow/tests/test_compute.py index 797fbc220ec3..0e98dd446a93 100644 --- a/python/pyarrow/tests/test_compute.py +++ b/python/pyarrow/tests/test_compute.py @@ -2255,6 +2255,34 @@ def test_cast(): assert pc.cast(arr, expected.type) == expected +def test_cast_struct_with_null_child(): + struct_type = pa.struct([ + pa.field('a', pa.int64(), nullable=False), + pa.field('n', pa.null()), + ]) + array = pa.array([[{'a': 1, 'n': None}, {'a': 2, 'n': None}]], + type=pa.list_(struct_type)) + struct_array = array.values + + sliced_struct = struct_array.slice(1, 1) + assert sliced_struct.cast(struct_type).validate(full=True) is None + + for casted in [struct_array.cast(struct_type), + pc.cast(struct_array, struct_type)]: + assert casted.validate(full=True) is None + + casted = array.cast(array.type) + assert casted.validate(full=True) is None + + chunked = pa.chunked_array([array]).cast(array.type) + for chunk in chunked.iterchunks(): + assert chunk.validate(full=True) is None + + record_batch = pa.RecordBatch.from_arrays([array], ['x']) + casted = record_batch.cast(pa.schema([pa.field('x', array.type)])) + assert casted.column(0).validate(full=True) is None + + @pytest.mark.parametrize('value_type', [pa.date32(), pa.date64()]) def test_identity_cast_dates(value_type): dt = datetime.date(1990, 3, 1)