From 64fdbe8149cbf406d64d9562cf753b814c5c8510 Mon Sep 17 00:00:00 2001 From: Nayana Naik Date: Mon, 19 Oct 2026 23:39:12 +0530 Subject: [PATCH] GH-44827: [C++] Skip null slots when transposing dictionary indices --- cpp/src/arrow/array/array_dict.cc | 25 +++++++++++++++------ cpp/src/arrow/array/array_dict_test.cc | 30 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/cpp/src/arrow/array/array_dict.cc b/cpp/src/arrow/array/array_dict.cc index 6b3511721d28..3dcadcf373b4 100644 --- a/cpp/src/arrow/array/array_dict.cc +++ b/cpp/src/arrow/array/array_dict.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "arrow/table.h" #include "arrow/type.h" #include "arrow/type_traits.h" +#include "arrow/util/bit_run_reader.h" #include "arrow/util/bit_util.h" #include "arrow/util/bitmap_ops.h" #include "arrow/util/checked_cast.h" @@ -187,9 +189,8 @@ Result> TransposeDictIndices( } // Default path: compute a buffer of transposed indices. - ARROW_ASSIGN_OR_RAISE( - auto out_buffer, - AllocateBuffer(data->length * (out_index_type.bit_width() / CHAR_BIT), pool)); + const int64_t out_buffer_size = data->length * (out_index_type.bit_width() / CHAR_BIT); + ARROW_ASSIGN_OR_RAISE(auto out_buffer, AllocateBuffer(out_buffer_size, pool)); // Shift null buffer if the original offset is non-zero std::shared_ptr null_bitmap; @@ -203,10 +204,20 @@ Result> TransposeDictIndices( auto out_data = ArrayData::Make(out_type, data->length, {null_bitmap, std::move(out_buffer)}, data->null_count); out_data->dictionary = dictionary; - RETURN_NOT_OK(internal::TransposeInts( - in_index_type, out_index_type, data->GetValues(1, 0), - out_data->GetMutableValues(1, 0), data->offset, out_data->offset, - data->length, transpose_map)); + const uint8_t* in_indices = data->GetValues(1, 0); + uint8_t* out_indices = out_data->GetMutableValues(1, 0); + // Indices under null slots are unspecified and may be out of bounds for + // transpose_map, so zero them and only transpose the valid runs. + if (data->MayHaveNulls()) { + std::memset(out_indices, 0, static_cast(out_buffer_size)); + } + RETURN_NOT_OK(internal::VisitSetBitRuns(data->buffers[0], data->offset, data->length, + [&](int64_t position, int64_t length) { + return internal::TransposeInts( + in_index_type, out_index_type, in_indices, + out_indices, data->offset + position, + position, length, transpose_map); + })); return out_data; } diff --git a/cpp/src/arrow/array/array_dict_test.cc b/cpp/src/arrow/array/array_dict_test.cc index 23335ebb008a..a24e508fcba0 100644 --- a/cpp/src/arrow/array/array_dict_test.cc +++ b/cpp/src/arrow/array/array_dict_test.cc @@ -1439,6 +1439,36 @@ TEST(TestDictionary, TransposeNulls) { CheckTranspose(sliced, transpose_map.data(), out_dict_type, out_dict, expected_indices); } +TEST(TestDictionary, TransposeNullsWithOutOfBoundsIndex) { + // Index values under null slots are not checked by ValidateFull(), + // so they must not be looked up in the transpose map. + auto dict = ArrayFromJSON(utf8(), "[\"A\", \"B\", \"C\"]"); + auto dict_type = dictionary(int32(), utf8()); + auto valid_indices = ArrayFromJSON(int32(), "[1, null, 0, null]"); + auto indices = MakeArray(ArrayData::Make( + int32(), 4, + {valid_indices->data()->buffers[0], + Buffer::FromVector(std::vector{1, 1 << 30, 0, -(1 << 30)})}, + /*null_count=*/2)); + auto arr = std::make_shared(dict_type, indices, dict); + ASSERT_OK(arr->ValidateFull()); + + auto out_dict = ArrayFromJSON(utf8(), "[\"Z\", \"A\", \"C\", \"B\"]"); + std::vector transpose_map = {1, 3, 2}; + CheckTranspose(arr, transpose_map.data(), dict_type, out_dict, + ArrayFromJSON(int32(), "[3, null, 1, null]")); + CheckTranspose(arr->Slice(1, 2), transpose_map.data(), dict_type, out_dict, + ArrayFromJSON(int32(), "[null, 1]")); + + ASSERT_OK_AND_ASSIGN(auto compacted, arr->Compact()); + ASSERT_OK(compacted->ValidateFull()); + ASSERT_OK_AND_ASSIGN( + auto expected, + DictionaryArray::FromArrays(dict_type, ArrayFromJSON(int32(), "[1, null, 0, null]"), + ArrayFromJSON(utf8(), "[\"A\", \"B\"]"))); + AssertArraysEqual(*compacted, *expected); +} + TEST(TestDictionary, ListOfDictionary) { std::unique_ptr root_builder; ASSERT_OK(MakeBuilder(default_memory_pool(), list(dictionary(int8(), utf8())),