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 f8dcfc734ba GH-47390: [C++][Acero] Allow for any type of scalar in 
Pivot longer features (#47391)
f8dcfc734ba is described below

commit f8dcfc734ba283b45a95f20176dbcf02b3c97051
Author: gitmodimo <[email protected]>
AuthorDate: Wed Aug 26 10:31:09 2026 +0200

    GH-47390: [C++][Acero] Allow for any type of scalar in Pivot longer 
features (#47391)
    
    ### Rationale for this change
    Allow supplying any scalar as feature
    
    ### What changes are included in this PR?
    Support scalar in PivotLonger
    
    ### Are these changes tested?
    Yes
    
    ### Are there any user-facing changes?
    Yes.
    
    **This PR includes breaking changes to public APIs.**
    
    `PivotLongerRowTemplate::feature_values` is now a 
`std::vector<std::shared_ptr<Scalar>>` while it used to be 
`std::vector<std::string>`).
    The `PivotLongerRowTemplate` still allows passing a vector of strings, 
though.
    
    * GitHub Issue: #47390
    
    Lead-authored-by: RafaƂ Hibner <[email protected]>
    Co-authored-by: gitmodimo <[email protected]>
    Co-authored-by: Rossi Sun <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/acero/options.cc                | 12 ++++++++
 cpp/src/arrow/acero/options.h                 |  6 ++--
 cpp/src/arrow/acero/pivot_longer_node.cc      | 31 ++++++++++++++++++---
 cpp/src/arrow/acero/pivot_longer_node_test.cc | 40 +++++++++++++++++++--------
 4 files changed, 71 insertions(+), 18 deletions(-)

diff --git a/cpp/src/arrow/acero/options.cc b/cpp/src/arrow/acero/options.cc
index 8bb1e10f3cb..40f7726ea76 100644
--- a/cpp/src/arrow/acero/options.cc
+++ b/cpp/src/arrow/acero/options.cc
@@ -18,6 +18,7 @@
 #include "arrow/acero/options.h"
 #include "arrow/acero/exec_plan.h"
 #include "arrow/io/util_internal.h"
+#include "arrow/scalar.h"
 #include "arrow/table.h"
 #include "arrow/util/async_generator.h"
 #include "arrow/util/logging.h"
@@ -62,6 +63,17 @@ ExecBatchIteratorMaker VecToItMaker(std::vector<ExecBatch> 
batches) {
 }
 }  // namespace
 
+PivotLongerRowTemplate::PivotLongerRowTemplate(
+    std::vector<std::string> feature_values,
+    std::vector<std::optional<FieldRef>> measurement_values)
+    : measurement_values(std::move(measurement_values)) {
+  this->feature_values.reserve(feature_values.size());
+  for (auto& feature_value : feature_values) {
+    this->feature_values.push_back(
+        std::make_shared<StringScalar>(std::move(feature_value)));
+  }
+}
+
 ExecBatchSourceNodeOptions::ExecBatchSourceNodeOptions(
     std::shared_ptr<Schema> schema, std::vector<ExecBatch> batches,
     ::arrow::internal::Executor* io_executor)
diff --git a/cpp/src/arrow/acero/options.h b/cpp/src/arrow/acero/options.h
index 827e9ea775d..8420793b992 100644
--- a/cpp/src/arrow/acero/options.h
+++ b/cpp/src/arrow/acero/options.h
@@ -780,15 +780,17 @@ class ARROW_ACERO_EXPORT TableSinkNodeOptions : public 
ExecNodeOptions {
 
 /// \brief a row template that describes one row that will be generated for 
each input row
 struct ARROW_ACERO_EXPORT PivotLongerRowTemplate {
-  PivotLongerRowTemplate(std::vector<std::string> feature_values,
+  PivotLongerRowTemplate(std::vector<std::shared_ptr<Scalar>> feature_values,
                          std::vector<std::optional<FieldRef>> 
measurement_values)
       : feature_values(std::move(feature_values)),
         measurement_values(std::move(measurement_values)) {}
+  PivotLongerRowTemplate(std::vector<std::string> feature_values,
+                         std::vector<std::optional<FieldRef>> 
measurement_values);
   /// A (typically unique) set of feature values for the template, usually 
derived from a
   /// column name
   ///
   /// These will be used to populate the feature columns
-  std::vector<std::string> feature_values;
+  std::vector<std::shared_ptr<Scalar>> feature_values;
   /// The fields containing the measurements to use for this row
   ///
   /// These will be used to populate the measurement columns.  If nullopt then 
nulls
diff --git a/cpp/src/arrow/acero/pivot_longer_node.cc 
b/cpp/src/arrow/acero/pivot_longer_node.cc
index c8f2a5c7b06..04b96dc8dcd 100644
--- a/cpp/src/arrow/acero/pivot_longer_node.cc
+++ b/cpp/src/arrow/acero/pivot_longer_node.cc
@@ -42,7 +42,7 @@ namespace {
 
 // A row template that's been bound to a schema
 struct BoundRowTemplate {
-  std::vector<std::string> feature_values;
+  std::vector<std::shared_ptr<Scalar>> feature_values;
   std::vector<std::optional<FieldPath>> measurement_paths;
 
   static Result<BoundRowTemplate> Make(const PivotLongerRowTemplate& unbound,
@@ -65,7 +65,7 @@ struct BoundRowTemplate {
   }
 
  private:
-  BoundRowTemplate(std::vector<std::string> feature_values,
+  BoundRowTemplate(std::vector<std::shared_ptr<Scalar>> feature_values,
                    std::vector<std::optional<FieldPath>> measurement_paths)
       : feature_values(std::move(feature_values)),
         measurement_paths(std::move(measurement_paths)) {}
@@ -89,6 +89,8 @@ class PivotLongerNode : public ExecNode, public TracedNode {
           "have names");
     }
 
+    std::vector<std::shared_ptr<DataType>> feature_types(
+        options.feature_field_names.size());
     for (const auto& row_template : options.row_templates) {
       if (row_template.feature_values.size() != 
options.feature_field_names.size()) {
         return Status::Invalid("There were names given for ",
@@ -103,11 +105,32 @@ class PivotLongerNode : public ExecNode, public 
TracedNode {
             " measurement columns but one of the row templates only had ",
             row_template.measurement_values.size(), " field references");
       }
+
+      for (std::size_t i = 0; i < row_template.feature_values.size(); i++) {
+        if (!row_template.feature_values[i]) {
+          return Status::Invalid("Feature value at column ",
+                                 options.feature_field_names[i], " must not be 
null");
+        }
+        if (feature_types[i]) {
+          if (!feature_types[i]->Equals(row_template.feature_values[i]->type)) 
{
+            return Status::TypeError(
+                "Mixed feature types at column ", 
options.feature_field_names[i],
+                ".  Some row templates had the type ", 
feature_types[i]->ToString(),
+                " but later row templates had the type ",
+                row_template.feature_values[i]->type->ToString(),
+                ".  All row templates must have same type for each feature "
+                "column.");
+          }
+        } else {
+          feature_types[i] = row_template.feature_values[i]->type;
+        }
+      }
     }
 
     std::vector<std::shared_ptr<Field>> fields(input_schema->fields());
-    for (const auto& name : options.feature_field_names) {
-      fields.push_back(field(name, utf8()));
+    for (std::size_t i = 0; i < options.feature_field_names.size(); i++) {
+      fields.push_back(
+          field(options.feature_field_names[i], std::move(feature_types[i])));
     }
     std::vector<std::shared_ptr<DataType>> measurement_types(
         options.measurement_field_names.size());
diff --git a/cpp/src/arrow/acero/pivot_longer_node_test.cc 
b/cpp/src/arrow/acero/pivot_longer_node_test.cc
index 9c548a2f23f..7b71f60be05 100644
--- a/cpp/src/arrow/acero/pivot_longer_node_test.cc
+++ b/cpp/src/arrow/acero/pivot_longer_node_test.cc
@@ -43,9 +43,15 @@ TEST(PivotLongerNode, Basic) {
           ->Table(kRowsPerBatch, kNumBatches);
 
   PivotLongerNodeOptions options;
-  options.feature_field_names = {"feature1", "feature2"};
+  options.feature_field_names = {"feature1", "feature2", "feature3"};
   options.measurement_field_names = {"meas1", "meas2"};
-  options.row_templates = {{{"a", "x"}, {{1}, {3}}}, {{"b", "y"}, {{2}, 
std::nullopt}}};
+  options.row_templates = {
+      {{std::make_shared<StringScalar>("a"), 
std::make_shared<StringScalar>("x"),
+        std::make_shared<UInt32Scalar>(12)},
+       {{1}, {3}}},
+      {{std::make_shared<StringScalar>("b"), 
std::make_shared<StringScalar>("y"),
+        std::make_shared<UInt32Scalar>(13)},
+       {{2}, std::nullopt}}};
 
   Declaration plan = Declaration::Sequence({
       {"table_source", TableSourceNodeOptions(std::move(input))},
@@ -62,6 +68,7 @@ TEST(PivotLongerNode, Basic) {
       field("f3", uint32()),
       field("feature1", utf8()),
       field("feature2", utf8()),
+      field("feature3", uint32()),
       field("meas1", uint32()),
       field("meas2", uint32()),
   });
@@ -70,7 +77,8 @@ TEST(PivotLongerNode, Basic) {
   AssertSchemaEqual(expected_out_schema, output->schema());
 }
 
-void CheckError(const PivotLongerNodeOptions& options, const std::string& 
message) {
+void CheckError(const PivotLongerNodeOptions& options, const std::string& 
message,
+                StatusCode code = StatusCode::Invalid) {
   std::shared_ptr<Table> input = gen::Gen({gen::Step(), 
gen::Random(boolean())})
                                      ->FailOnError()
                                      ->Table(/*rows_per_chunk=*/1, 
/*num_chunks=*/1);
@@ -81,19 +89,19 @@ void CheckError(const PivotLongerNodeOptions& options, 
const std::string& messag
   });
 
   ASSERT_THAT(DeclarationToStatus(std::move(plan)),
-              Raises(StatusCode::Invalid, testing::HasSubstr(message)));
+              Raises(code, testing::HasSubstr(message)));
 }
 
 TEST(PivotLongerNode, Error) {
   PivotLongerNodeOptions options;
   CheckError(options, "There must be at least one row template");
 
-  options.row_templates = {{{}, {{0}}}};
+  options.row_templates = {{std::vector<std::string>{}, {{0}}}};
   CheckError(options, "at least one feature column and one measurement 
column");
 
   options.feature_field_names = {"feat1"};
   options.measurement_field_names = {"meas1"};
-  options.row_templates = {{{}, {{0}}}};
+  options.row_templates = {{std::vector<std::string>{}, {{0}}}};
   CheckError(options,
              "There were names given for 1 feature columns but one of the row 
templates "
              "only had 0 feature values");
@@ -111,6 +119,13 @@ TEST(PivotLongerNode, Error) {
 
   options.row_templates = {{{"x"}, {std::nullopt}}, {{"y"}, {std::nullopt}}};
   CheckError(options, "All row templates had nullopt");
+
+  options.row_templates = {{{std::make_shared<StringScalar>("x")}, {{0}}},
+                           {{std::make_shared<UInt32Scalar>(1)}, {{0}}}};
+  CheckError(options,
+             "Some row templates had the type string but later row templates 
had the "
+             "type uint32",
+             StatusCode::TypeError);
 }
 
 // The following examples are smaller versions of examples taken from
@@ -183,7 +198,8 @@ TEST(PivotLongerNode, ExamplesFromTidyr2) {
   PivotLongerNodeOptions options;
   options.feature_field_names = {"week"};
   options.measurement_field_names = {"rank"};
-  options.row_templates = {{{"1"}, {{2}}}, {{"2"}, {{3}}}};
+  options.row_templates = {{{std::make_shared<UInt32Scalar>(1)}, {{2}}},
+                           {{std::make_shared<UInt32Scalar>(2)}, {{3}}}};
 
   Declaration plan = Declaration::Sequence(
       {{"table_source", TableSourceNodeOptions(std::move(input))},
@@ -196,14 +212,14 @@ TEST(PivotLongerNode, ExamplesFromTidyr2) {
                        DeclarationToTable(std::move(plan)));
 
   std::shared_ptr<Schema> expected_schema =
-      schema({field("artist", utf8()), field("track", utf8()), field("week", 
utf8()),
+      schema({field("artist", utf8()), field("track", utf8()), field("week", 
uint32()),
               field("rank", float64())});
   std::shared_ptr<Table> expected = TableFromJSON(expected_schema, {{
                                                                        R"([
-        ["2 Pac", "Baby Don't Cry", "1", 87],
-        ["2Ge+her", "The Hardest Part Of", "1", 91],
-        ["2 Pac", "Baby Don't Cry", "2", 82],
-        ["2Ge+her", "The Hardest Part Of", "2", 87]
+        ["2 Pac", "Baby Don't Cry", 1, 87],
+        ["2Ge+her", "The Hardest Part Of", 1, 91],
+        ["2 Pac", "Baby Don't Cry", 2, 82],
+        ["2Ge+her", "The Hardest Part Of", 2, 87]
     ])"}});
 
   AssertTablesEqual(*expected, *output, /*same_chunk_layout=*/false);

Reply via email to