This is an automated email from the ASF dual-hosted git repository.

pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 34647d278a GH-48679: [C++] Fix pivot_wider with non-monotonic group 
ids (#50423)
34647d278a is described below

commit 34647d278ab997007ca70aa880a6f1a2128905de
Author: Antoine Pitrou <[email protected]>
AuthorDate: Wed Jul 22 11:07:57 2026 +0200

    GH-48679: [C++] Fix pivot_wider with non-monotonic group ids (#50423)
    
    ### Rationale for this change
    
    The pivot_wider implementation was assuming that the `Grouper` produces 
monotonic group ids for the pivot key names.
    
    However, that turns out to not always be true when the `FastGrouperImpl` is 
involved (perhaps when hash collisions are involved?).
    
    ### What changes are included in this PR?
    
    Compute and apply a mapping from pivot key group ids back to pivot key 
indices.
    
    ### Are these changes tested?
    
    Yes, by additional tests.
    
    ### Are there any user-facing changes?
    
    Only a bugfix.
    
    * GitHub Issue: #48679
    
    Authored-by: Antoine Pitrou <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/acero/hash_aggregate_test.cc      | 77 +++++++++++++++++++++++++
 cpp/src/arrow/compute/kernels/aggregate_test.cc | 34 +++++++++++
 cpp/src/arrow/compute/kernels/pivot_internal.cc | 37 ++++++++++--
 3 files changed, 144 insertions(+), 4 deletions(-)

diff --git a/cpp/src/arrow/acero/hash_aggregate_test.cc 
b/cpp/src/arrow/acero/hash_aggregate_test.cc
index 442dcd5883..2f498f3a7b 100644
--- a/cpp/src/arrow/acero/hash_aggregate_test.cc
+++ b/cpp/src/arrow/acero/hash_aggregate_test.cc
@@ -4819,6 +4819,83 @@ TEST_P(GroupBy, PivotScalarKey) {
   }
 }
 
+TEST_P(GroupBy, PivotNonMonotonicGroupId) {
+  // Hard-coded test for GH-48679: FastGrouperImpl can yield non-mononotonic 
group ids
+  // and the pivot_wider implementation has to account for that.
+
+  // NOTE The precise keys to trigger this situation rely on implementation 
details
+  // of FastGrouperImpl. Any internal change might lead to this test not 
exercising
+  // the desired situation anymore.
+  auto key_type = utf8();
+  auto value_type = float32();
+  std::vector<std::string> table_json = {
+      R"([
+        [1, "k", 10.5],
+        [2, "l", 11.5]
+      ])",
+      R"([
+        [2, "m", 12.5]
+      ])",
+      R"([
+        [3, "k", 13.5],
+        [1, "n", 14.5],
+        [1, "o", 15.5]
+      ])"};
+  std::string expected_json = R"([
+      [1, {"k": 10.5, "n": 14.5, "o": 15.5} ],
+      [2, {"l": 11.5, "m": 12.5} ],
+      [3, {"k": 13.5} ]
+  ])";
+  for (auto unexpected_key_behavior :
+       {PivotWiderOptions::kIgnore, PivotWiderOptions::kRaise}) {
+    PivotWiderOptions options(/*key_names=*/{"k", "l", "m", "n", "o"},
+                              unexpected_key_behavior);
+    TestPivot(key_type, value_type, options, table_json, expected_json);
+  }
+}
+
+TEST_P(GroupBy, PivotNonMonotonicGroupIdWithScalarKey) {
+  // Like PivotNonMonotonicGroupId, but with a scalar key.
+  BatchesWithSchema input;
+  std::vector<TypeHolder> types = {int32(), utf8(), float32()};
+  std::vector<ArgShape> shapes = {ArgShape::ARRAY, ArgShape::SCALAR, 
ArgShape::ARRAY};
+  input.batches = {
+      ExecBatchFromJSON(types, shapes, R"([
+        [1, "m", 10.5],
+        [2, "m", 11.5]
+        ])"),
+      ExecBatchFromJSON(types, shapes, R"([
+        [2, "o", 12.5]
+        ])"),
+      ExecBatchFromJSON(types, shapes, R"([
+        [3, "n", 13.5],
+        [1, "n", 14.5]
+        ])"),
+  };
+  input.schema = schema({field("group_key", int32()), field("pivot_key", 
utf8()),
+                         field("pivot_value", float32())});
+  Datum expected = ArrayFromJSON(
+      struct_({field("group_key", int32()),
+               field("pivoted", struct_({field("m", float32()), field("n", 
float32()),
+                                         field("o", float32())}))}),
+      R"([
+        [1, {"m": 10.5, "n": 14.5} ],
+        [2, {"m": 11.5, "o": 12.5} ],
+        [3, {"n": 13.5} ]
+      ])");
+  auto options = std::make_shared<PivotWiderOptions>(
+      PivotWiderOptions(/*key_names=*/{"m", "n", "o"}));
+  Aggregate aggregate{"hash_pivot_wider", options,
+                      std::vector<FieldRef>{"pivot_key", "pivot_value"}, 
"pivoted"};
+  for (bool use_threads : {false, true}) {
+    SCOPED_TRACE(use_threads ? "parallel/merged" : "serial");
+    ASSERT_OK_AND_ASSIGN(Datum actual,
+                         RunGroupBy(input, {"group_key"}, {aggregate}, 
use_threads));
+    ValidateOutput(actual);
+    AssertDatumsApproxEqual(expected, actual, /*verbose=*/true);
+  }
+}
+
 TEST_P(GroupBy, PivotUnusedKeyName) {
   auto key_type = utf8();
   auto value_type = float32();
diff --git a/cpp/src/arrow/compute/kernels/aggregate_test.cc 
b/cpp/src/arrow/compute/kernels/aggregate_test.cc
index bfc3442a73..4ef2b2f88a 100644
--- a/cpp/src/arrow/compute/kernels/aggregate_test.cc
+++ b/cpp/src/arrow/compute/kernels/aggregate_test.cc
@@ -4762,6 +4762,40 @@ TEST_F(TestPivotKernel, ScalarValue) {
               PivotWiderOptions(/*key_names=*/{"height", "width"}));
 }
 
+TEST_F(TestPivotKernel, NonMonotonicGroupId) {
+  // Hard-coded test for GH-48679: FastGrouperImpl can yield non-mononotonic 
group ids
+  // and the pivot_wider implementation has to account for that.
+
+  // NOTE The precise keys to trigger this situation rely on implementation 
details
+  // of FastGrouperImpl. Any internal change might lead to this test not 
exercising
+  // the desired situation anymore.
+  // (see similar test in hash_aggregate_test.cc)
+
+  auto key_type = utf8();
+  auto value_type = int16();
+  auto keys = ArrayFromJSON(key_type, R"(["m", "n", "o"])");
+  auto values = ArrayFromJSON(value_type, "[10, 11, 12]");
+  auto expected = ScalarFromJSON(
+      struct_({field("m", value_type), field("n", value_type), field("o", 
value_type)}),
+      "[10, 11, 12]");
+  AssertPivot(keys, values, *expected, PivotWiderOptions(/*key_names=*/{"m", 
"n", "o"}));
+}
+
+TEST_F(TestPivotKernel, NonMonotonicGroupIdWithScalarKey) {
+  // Like NonMonotonicGroupId, but with a scalar key.
+  // Even with a single key in the data, the presence of several keys in 
key_names
+  // can still trigger the issue.
+  auto key_type = utf8();
+  auto value_type = int16();
+
+  auto keys = ScalarFromJSON(key_type, R"("o")");
+  auto values = ArrayFromJSON(value_type, "[null, 11, null]");
+  auto expected = ScalarFromJSON(
+      struct_({field("m", value_type), field("n", value_type), field("o", 
value_type)}),
+      "[null, null, 11]");
+  AssertPivot(keys, values, *expected, PivotWiderOptions(/*key_names=*/{"m", 
"n", "o"}));
+}
+
 TEST_F(TestPivotKernel, EmptyInput) {
   auto key_type = utf8();
   auto value_type = float32();
diff --git a/cpp/src/arrow/compute/kernels/pivot_internal.cc 
b/cpp/src/arrow/compute/kernels/pivot_internal.cc
index c9ccc7d607..8350edba12 100644
--- a/cpp/src/arrow/compute/kernels/pivot_internal.cc
+++ b/cpp/src/arrow/compute/kernels/pivot_internal.cc
@@ -23,10 +23,11 @@
 
 #include "arrow/array/array_primitive.h"
 #include "arrow/array/builder_binary.h"
+#include "arrow/compute/api_vector.h"
 #include "arrow/compute/cast.h"
 #include "arrow/compute/exec.h"
-#include "arrow/compute/kernels/codegen_internal.h"
 #include "arrow/compute/row/grouper.h"
+#include "arrow/result.h"
 #include "arrow/scalar.h"
 #include "arrow/type_traits.h"
 #include "arrow/util/bit_run_reader.h"
@@ -45,6 +46,7 @@ struct ConcretePivotWiderKeyMapper : public 
PivotWiderKeyMapper {
                                     static_cast<size_t>(kMaxPivotKey), " 
columns: got ",
                                     options->key_names.size());
     }
+    ctx_ = ctx;
     unexpected_key_behavior_ = options->unexpected_key_behavior;
     ARROW_ASSIGN_OR_RAISE(grouper_, Grouper::Make({&key_type}, ctx));
     // Build a binary array of the pivot key values, and cast it to the 
desired key type
@@ -61,9 +63,9 @@ struct ConcretePivotWiderKeyMapper : public 
PivotWiderKeyMapper {
     ARROW_ASSIGN_OR_RAISE(auto binary_key_array, builder.Finish());
     ARROW_ASSIGN_OR_RAISE(auto key_array,
                           Cast(*binary_key_array, &key_type, 
CastOptions::Safe(), ctx));
-    // Populate the grouper with the keys from the array
+    // Populate the grouper with the keys from the array, and get the key 
group ids
     ExecSpan batch({ExecValue(*key_array->data())}, key_array->length());
-    RETURN_NOT_OK(grouper_->Populate(batch));
+    ARROW_ASSIGN_OR_RAISE(auto key_indices_to_group_ids, 
grouper_->Consume(batch));
     if (grouper_->num_groups() != options->key_names.size()) {
       // There's a duplicate key, find it to emit a nicer error message
       std::unordered_set<std::string_view> seen;
@@ -75,6 +77,23 @@ struct ConcretePivotWiderKeyMapper : public 
PivotWiderKeyMapper {
       }
       Unreachable("Grouper doesn't agree with std::unordered_set");
     }
+    // GH-48679: the fast grouper implementation may produce non-monotonic
+    // group ids, for example [0,1,2,4,3] rather than [0,1,2,3,4].
+    // Therefore, we need to produce a mapping of group ids to key indices.
+    // TODO: revisit this if the fast grouper is amended to guarantee monotonic
+    // group ids.
+    auto key_indices_to_group_ids_data = 
key_indices_to_group_ids.array()->Copy();
+    // InversePermutation doesn't allow unsigned integers, patch to signed.
+    DCHECK_EQ(key_indices_to_group_ids_data->type->id(), Type::UINT32);
+    key_indices_to_group_ids_data->type = int32();
+    ARROW_ASSIGN_OR_RAISE(auto group_ids_to_key_indices,
+                          InversePermutation(key_indices_to_group_ids_data,
+                                             
InversePermutationOptions::Defaults(), ctx));
+    auto group_ids_to_key_indices_data = 
group_ids_to_key_indices.array()->Copy();
+    group_ids_to_key_indices_data->type = uint32();
+    DCHECK_EQ(group_ids_to_key_indices.length(), grouper_->num_groups());
+    DCHECK_EQ(group_ids_to_key_indices.null_count(), 0);
+    group_ids_to_key_indices_ = 
Datum(std::move(group_ids_to_key_indices_data));
     return Status::OK();
   }
 
@@ -134,12 +153,22 @@ struct ConcretePivotWiderKeyMapper : public 
PivotWiderKeyMapper {
       }
       return Status::KeyError("Unexpected pivot key: ", 
key_scalar->ToString());
     }
-    return group_id_array;
+    // Map back the group ids to indices in the original keys array
+    // NOTE Instead of materializing the Take result here, we could instead 
expose
+    // the group_ids_to_key_indices_ mapping to the caller and let them
+    // apply the mapping as needed. This would spare a memory allocation.
+    ARROW_ASSIGN_OR_RAISE(result, Take(group_ids_to_key_indices_, result,
+                                       TakeOptions::NoBoundsCheck(), ctx_));
+    DCHECK(result.is_array());
+    DCHECK_EQ(result.type()->id(), Type::UINT32);
+    return result.array();
   }
 
   Status NullKeyName() { return Status::KeyError("pivot key name cannot be 
null"); }
 
+  ExecContext* ctx_;
   std::unique_ptr<Grouper> grouper_;
+  Datum group_ids_to_key_indices_;
   PivotWiderOptions::UnexpectedKeyBehavior unexpected_key_behavior_;
   std::shared_ptr<Buffer> last_group_ids_;
 };

Reply via email to