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

kou 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 83b0e198b3 GH-49977: [C++][Gandiva] Add regexp_extract optional third 
parameter function version (#49978)
83b0e198b3 is described below

commit 83b0e198b37b575d0cca6f5263c3f0578dc616a3
Author: Logan Riggs <[email protected]>
AuthorDate: Thu Aug 13 14:32:41 2026 -0700

    GH-49977: [C++][Gandiva] Add regexp_extract optional third parameter 
function version (#49978)
    
    ### Rationale for this change
    The existing 3 argument REGEXP_EXTRACT function requires the group index to 
return. It would be useful to have a 2 arg version of the function where the 
index defaults to 1. This would align well with other comparable database 
systems.
    
    ### What changes are included in this PR?
    A new 2 arg REGEXP_FUNCTION and unit tests.
    
    ### Are these changes tested?
    Yes, unit test and manual testing.
    
    ### Are there any user-facing changes?
    Yes, a new 2 argument REGEXP_FUNCTION.
    * GitHub Issue: #49977
    
    Authored-by: [email protected] <[email protected]>
    Signed-off-by: Sutou Kouhei <[email protected]>
---
 cpp/src/gandiva/function_registry_string.cc    |   6 ++
 cpp/src/gandiva/gdv_string_function_stubs.cc   |  25 +++++
 cpp/src/gandiva/regex_functions_holder.cc      |   4 +-
 cpp/src/gandiva/regex_functions_holder_test.cc | 141 ++++++++++++++++++++++++-
 cpp/src/gandiva/tests/projector_test.cc        |  41 +++++++
 5 files changed, 211 insertions(+), 6 deletions(-)

diff --git a/cpp/src/gandiva/function_registry_string.cc 
b/cpp/src/gandiva/function_registry_string.cc
index bce317ade8..35938cb829 100644
--- a/cpp/src/gandiva/function_registry_string.cc
+++ b/cpp/src/gandiva/function_registry_string.cc
@@ -257,6 +257,12 @@ std::vector<NativeFunction> GetStringFunctionRegistry() {
                          NativeFunction::kNeedsFunctionHolder |
                          NativeFunction::kCanReturnErrors),
 
+      NativeFunction("regexp_extract", {}, DataTypeVector{utf8(), utf8()}, 
utf8(),
+                     kResultNullIfNull, "gdv_fn_regexp_extract_utf8_utf8",
+                     NativeFunction::kNeedsContext |
+                         NativeFunction::kNeedsFunctionHolder |
+                         NativeFunction::kCanReturnErrors),
+
       NativeFunction("regexp_extract", {}, DataTypeVector{utf8(), utf8(), 
int32()},
                      utf8(), kResultNullIfNull, 
"gdv_fn_regexp_extract_utf8_utf8_int32",
                      NativeFunction::kNeedsContext |
diff --git a/cpp/src/gandiva/gdv_string_function_stubs.cc 
b/cpp/src/gandiva/gdv_string_function_stubs.cc
index a0936d448a..55f5f13ac3 100644
--- a/cpp/src/gandiva/gdv_string_function_stubs.cc
+++ b/cpp/src/gandiva/gdv_string_function_stubs.cc
@@ -70,6 +70,18 @@ const char* gdv_fn_regexp_replace_utf8_utf8(
                    out_length);
 }
 
+GANDIVA_EXPORT
+const char* gdv_fn_regexp_extract_utf8_utf8(int64_t ptr, int64_t holder_ptr,
+                                            const char* data, int32_t data_len,
+                                            const char* /*pattern*/,
+                                            int32_t /*pattern_len*/,
+                                            int32_t* out_length) {
+  gandiva::ExecutionContext* context = 
reinterpret_cast<gandiva::ExecutionContext*>(ptr);
+  gandiva::ExtractHolder* holder = 
reinterpret_cast<gandiva::ExtractHolder*>(holder_ptr);
+  return (*holder)(context, data, data_len, 1, out_length);
+}
+
+GANDIVA_EXPORT
 const char* gdv_fn_regexp_extract_utf8_utf8_int32(int64_t ptr, int64_t 
holder_ptr,
                                                   const char* data, int32_t 
data_len,
                                                   const char* /*pattern*/,
@@ -924,6 +936,19 @@ arrow::Status ExportedStringFunctions::AddMappings(Engine* 
engine) const {
       "gdv_fn_regexp_extract_utf8_utf8_int32", types->i8_ptr_type() 
/*return_type*/, args,
       reinterpret_cast<void*>(gdv_fn_regexp_extract_utf8_utf8_int32));
 
+  // gdv_fn_regexp_extract_utf8_utf8
+  args = {types->i64_type(),       // int64_t ptr
+          types->i64_type(),       // int64_t holder_ptr
+          types->i8_ptr_type(),    // const char* data
+          types->i32_type(),       // int data_len
+          types->i8_ptr_type(),    // const char* pattern
+          types->i32_type(),       // int pattern_len
+          types->i32_ptr_type()};  // int32_t* out_length
+
+  engine->AddGlobalMappingForFunc(
+      "gdv_fn_regexp_extract_utf8_utf8", types->i8_ptr_type() /*return_type*/, 
args,
+      reinterpret_cast<void*>(gdv_fn_regexp_extract_utf8_utf8));
+
   // gdv_fn_castVARCHAR_int32_int64
   args = {types->i64_type(),       // int64_t execution_context
           types->i32_type(),       // int32_t value
diff --git a/cpp/src/gandiva/regex_functions_holder.cc 
b/cpp/src/gandiva/regex_functions_holder.cc
index e6bd09be37..b451e33d6f 100644
--- a/cpp/src/gandiva/regex_functions_holder.cc
+++ b/cpp/src/gandiva/regex_functions_holder.cc
@@ -212,8 +212,8 @@ void ReplaceHolder::return_error(ExecutionContext* context, 
std::string& data,
 }
 
 Result<std::shared_ptr<ExtractHolder>> ExtractHolder::Make(const FunctionNode& 
node) {
-  ARROW_RETURN_IF(node.children().size() != 3,
-                  Status::Invalid("'extract' function requires three 
parameters"));
+  ARROW_RETURN_IF(node.children().size() != 2 && node.children().size() != 3,
+                  Status::Invalid("'extract' function requires two or three 
parameters"));
 
   auto literal = dynamic_cast<LiteralNode*>(node.children().at(1).get());
   ARROW_RETURN_IF(
diff --git a/cpp/src/gandiva/regex_functions_holder_test.cc 
b/cpp/src/gandiva/regex_functions_holder_test.cc
index 4d7b0fd319..a78206bb84 100644
--- a/cpp/src/gandiva/regex_functions_holder_test.cc
+++ b/cpp/src/gandiva/regex_functions_holder_test.cc
@@ -604,24 +604,100 @@ TEST_F(TestExtractHolder, TestExtractInvalidPattern) {
   execution_context_.Reset();
 }
 
-TEST_F(TestExtractHolder, TestErrorWhileBuildingHolder) {
-  // Create function with incorrect number of params
+TEST_F(TestExtractHolder, TestEmptyInput) {
+  EXPECT_OK_AND_ASSIGN(auto extract_holder, ExtractHolder::Make(R"((\w+))"));
+  auto& extract = *extract_holder;
+  int32_t out_length = 0;
+
+  const char* ret = extract(&execution_context_, "", 0, 0, &out_length);
+  EXPECT_EQ(std::string(ret, out_length), "");
+  EXPECT_FALSE(execution_context_.has_error());
+}
+
+TEST_F(TestExtractHolder, TestOptionalGroup) {
+  // (a)?(b): group 1 is optional; when input is "b" it doesn't participate
+  EXPECT_OK_AND_ASSIGN(auto extract_holder, ExtractHolder::Make(R"((a)?(b))"));
+  auto& extract = *extract_holder;
+  int32_t out_length = 0;
+
+  std::string input = "b";
+  const char* ret = extract(&execution_context_, input.c_str(),
+                            static_cast<int32_t>(input.size()), 1, 
&out_length);
+  EXPECT_EQ(std::string(ret, out_length), "");
+  EXPECT_FALSE(execution_context_.has_error());
+
+  ret = extract(&execution_context_, input.c_str(), 
static_cast<int32_t>(input.size()), 2,
+                &out_length);
+  EXPECT_EQ(std::string(ret, out_length), "b");
+
+  input = "ab";
+  ret = extract(&execution_context_, input.c_str(), 
static_cast<int32_t>(input.size()), 1,
+                &out_length);
+  EXPECT_EQ(std::string(ret, out_length), "a");
+}
+
+TEST_F(TestExtractHolder, TestNoUserGroups) {
+  // Pattern with no user capturing groups — only the outer wrapper group 
exists.
+  // Index 0 returns the full match; index 1 is out of range.
+  EXPECT_OK_AND_ASSIGN(auto extract_holder, ExtractHolder::Make(R"(\d+)"));
+  auto& extract = *extract_holder;
+  int32_t out_length = 0;
+
+  std::string input = "abc123def";
+  const char* ret = extract(&execution_context_, input.c_str(),
+                            static_cast<int32_t>(input.size()), 0, 
&out_length);
+  EXPECT_EQ(std::string(ret, out_length), "123");
+  EXPECT_FALSE(execution_context_.has_error());
+
+  ret = extract(&execution_context_, input.c_str(), 
static_cast<int32_t>(input.size()), 1,
+                &out_length);
+  EXPECT_EQ(out_length, 0);
+  EXPECT_TRUE(execution_context_.has_error());
+  execution_context_.Reset();
+}
+
+TEST_F(TestExtractHolder, TestDefaultIndexExtract) {
+  // 2-arg form defaults to index 1 (first capture group)
   auto field = std::make_shared<FieldNode>(arrow::field("in", arrow::utf8()));
   auto pattern_node = std::make_shared<LiteralNode>(
       arrow::utf8(), LiteralHolder(R"((\w+) (\w+))"), false);
   auto function_node =
       FunctionNode("regexp_extract", {field, pattern_node}, arrow::utf8());
 
+  EXPECT_OK_AND_ASSIGN(auto extract_holder, 
ExtractHolder::Make(function_node));
+
+  std::string input_string = "John Doe";
+  int32_t out_length = 0;
+
+  auto& extract = *extract_holder;
+  const char* ret = extract(&execution_context_, input_string.c_str(),
+                            static_cast<int32_t>(input_string.length()), 1, 
&out_length);
+  EXPECT_EQ(std::string(ret, out_length), "John");
+
+  input_string = "Ringo Beast";
+  ret = extract(&execution_context_, input_string.c_str(),
+                static_cast<int32_t>(input_string.length()), 1, &out_length);
+  EXPECT_EQ(std::string(ret, out_length), "Ringo");
+}
+
+TEST_F(TestExtractHolder, TestErrorWhileBuildingHolder) {
+  // Create function with incorrect number of params (one arg)
+  auto field = std::make_shared<FieldNode>(arrow::field("in", arrow::utf8()));
+  NodeVector one_arg = {field};
+  auto function_node = FunctionNode("regexp_extract", one_arg, arrow::utf8());
+
   auto extract_holder = ExtractHolder::Make(function_node);
   EXPECT_RAISES_WITH_MESSAGE_THAT(
-      Invalid, ::testing::HasSubstr("'extract' function requires three 
parameters"),
+      Invalid,
+      ::testing::HasSubstr("'extract' function requires two or three 
parameters"),
       extract_holder.status());
 
   execution_context_.Reset();
 
   // Create function with non-utf8 literal parameter as pattern
   field = std::make_shared<FieldNode>(arrow::field("in", arrow::utf8()));
-  pattern_node = std::make_shared<LiteralNode>(arrow::int32(), 
LiteralHolder(2), false);
+  auto pattern_node =
+      std::make_shared<LiteralNode>(arrow::int32(), LiteralHolder(2), false);
   auto index_node = std::make_shared<FieldNode>(arrow::field("idx", 
arrow::int32()));
   function_node =
       FunctionNode("regexp_extract", {field, pattern_node, index_node}, 
arrow::utf8());
@@ -654,3 +730,60 @@ TEST_F(TestExtractHolder, TestErrorWhileBuildingHolder) {
 }
 
 }  // namespace gandiva
+
+extern "C" const char* gdv_fn_regexp_extract_utf8_utf8(int64_t ptr, int64_t 
holder_ptr,
+                                                       const char* data, 
int32_t data_len,
+                                                       const char* pattern,
+                                                       int32_t pattern_len,
+                                                       int32_t* out_length);
+
+TEST(TestRegexpExtractStub, TestDefaultIndexStub) {
+  gandiva::ExecutionContext ctx;
+  auto ctx_ptr = reinterpret_cast<int64_t>(&ctx);
+
+  EXPECT_OK_AND_ASSIGN(auto holder, gandiva::ExtractHolder::Make(R"((\w+) 
(\w+))"));
+  auto holder_ptr = reinterpret_cast<int64_t>(holder.get());
+
+  std::string pattern = R"((\w+) (\w+))";
+  int32_t out_length = 0;
+
+  std::string input = "John Doe";
+  const char* ret = gdv_fn_regexp_extract_utf8_utf8(
+      ctx_ptr, holder_ptr, input.c_str(), static_cast<int32_t>(input.size()),
+      pattern.c_str(), static_cast<int32_t>(pattern.size()), &out_length);
+  EXPECT_EQ(std::string(ret, out_length), "John");
+
+  input = "Ringo Beast";
+  ret = gdv_fn_regexp_extract_utf8_utf8(
+      ctx_ptr, holder_ptr, input.c_str(), static_cast<int32_t>(input.size()),
+      pattern.c_str(), static_cast<int32_t>(pattern.size()), &out_length);
+  EXPECT_EQ(std::string(ret, out_length), "Ringo");
+
+  // no match returns empty string
+  input = "--- ---";
+  ret = gdv_fn_regexp_extract_utf8_utf8(
+      ctx_ptr, holder_ptr, input.c_str(), static_cast<int32_t>(input.size()),
+      pattern.c_str(), static_cast<int32_t>(pattern.size()), &out_length);
+  EXPECT_EQ(out_length, 0);
+}
+
+extern "C" const char* gdv_fn_regexp_extract_utf8_utf8_int32(
+    int64_t ptr, int64_t holder_ptr, const char* data, int32_t data_len,
+    const char* pattern, int32_t pattern_len, int32_t extract_index, int32_t* 
out_length);
+
+TEST(TestRegexpExtractStub, TestIndexStub) {
+  gandiva::ExecutionContext ctx;
+  auto ctx_ptr = reinterpret_cast<int64_t>(&ctx);
+
+  EXPECT_OK_AND_ASSIGN(auto holder, gandiva::ExtractHolder::Make(R"((\w+) 
(\w+))"));
+  auto holder_ptr = reinterpret_cast<int64_t>(holder.get());
+
+  std::string pattern = R"((\w+) (\w+))";
+  int32_t out_length = 0;
+
+  std::string input = "John Doe";
+  const char* ret = gdv_fn_regexp_extract_utf8_utf8_int32(
+      ctx_ptr, holder_ptr, input.c_str(), static_cast<int32_t>(input.size()),
+      pattern.c_str(), static_cast<int32_t>(pattern.size()), 2, &out_length);
+  EXPECT_EQ(std::string(ret, out_length), "Doe");
+}
diff --git a/cpp/src/gandiva/tests/projector_test.cc 
b/cpp/src/gandiva/tests/projector_test.cc
index 27beb8595c..6e9450e1c6 100644
--- a/cpp/src/gandiva/tests/projector_test.cc
+++ b/cpp/src/gandiva/tests/projector_test.cc
@@ -3066,6 +3066,47 @@ TEST_F(TestProjector, TestRegexpExtract) {
   EXPECT_ARROW_ARRAY_EQUALS(exp_extract, outputs.at(0));
 }
 
+TEST_F(TestProjector, TestRegexpExtractTwoArg) {
+  // schema for input fields
+  auto field0 = field("f0", arrow::utf8());
+  auto schema = arrow::schema({field0});
+
+  // output fields
+  auto field_extract = field("extract", arrow::utf8());
+
+  // The two-arg overload defaults to extracting the first capture group 
(index 1).
+  std::string pattern(R"((\w+) (\w+))");
+  auto literal = TreeExprBuilder::MakeStringLiteral(pattern);
+  auto node0 = TreeExprBuilder::MakeField(field0);
+
+  // Build expression with the two-arg overload: regexp_extract(string, 
pattern)
+  auto regexp_extract_func =
+      TreeExprBuilder::MakeFunction("regexp_extract", {node0, literal}, 
arrow::utf8());
+  auto extract_expr = TreeExprBuilder::MakeExpression(regexp_extract_func, 
field_extract);
+
+  std::shared_ptr<Projector> projector;
+  auto status = Projector::Make(schema, {extract_expr}, TestConfiguration(), 
&projector);
+  EXPECT_TRUE(status.ok()) << status.message();
+
+  // Create a row-batch with some sample data
+  int num_records = 3;
+  auto array0 = MakeArrowArrayUtf8({"John Doe", "Ringo Beast", 
"stringthatdonotmatch"},
+                                   {true, true, true});
+  // expected output: first capture group, empty string when the pattern does 
not match
+  auto exp_extract = MakeArrowArrayUtf8({"John", "Ringo", ""}, {true, true, 
true});
+
+  // prepare input record batch
+  auto in = arrow::RecordBatch::Make(schema, num_records, {array0});
+
+  // Evaluate expression
+  arrow::ArrayVector outputs;
+  status = projector->Evaluate(*in, pool_, &outputs);
+  EXPECT_TRUE(status.ok()) << status.message();
+
+  // Validate results
+  EXPECT_ARROW_ARRAY_EQUALS(exp_extract, outputs.at(0));
+}
+
 TEST_F(TestProjector, TestCastVarbinary) {
   auto field0 = field("f0", arrow::utf8());
   auto field1 = field("f1", arrow::int64());

Reply via email to