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

sarutak pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 666afb88e670 [SPARK-57932][4.2][SQL] Fix regexp_instr and 
regexp_replace returning wrong results for supplementary characters
666afb88e670 is described below

commit 666afb88e670db245889a3aaa474c08d4a0f52e1
Author: Kousuke Saruta <[email protected]>
AuthorDate: Tue Jul 14 08:41:32 2026 +0900

    [SPARK-57932][4.2][SQL] Fix regexp_instr and regexp_replace returning wrong 
results for supplementary characters
    
    ### What changes were proposed in this pull request?
    This PR backports SPARK-57932 (#56998) for `branch-4.2` with conflict 
resolution.
    
    `regexp_instr` returned matcher.start() + 1 (a UTF-16 code-unit index), and 
`regexp_replace`'s position argument was used as a UTF-16 offset in 
matcher.region(pos-1, ...) and its out-of-range guard. Both now use code-point 
positions via codePointCount/offsetByCodePoints.
    
    ### Why are the changes needed?
    Spark string positions are 1-based code points (instr, locate, position, 
substring, length). For strings containing supplementary characters (code 
points > U+FFFF, e.g. emojis), these two functions gave wrong results:
    - regexp_instr('😀ab', 'ab') returned 3 instead of 2.
    - regexp_replace('😀aXa', 'a', 'Z', 3) returned 😀ZXZ instead of 😀aXZ 
(replaced a match before position); regexp_replace('😀😀', '😀', 'Z', 3) returned 
😀Z instead of the input unchanged. This is the sibling of the LIKE fix in 
SPARK-55453.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes. regexp_instr and regexp_replace now return code-point-consistent 
results for strings with supplementary characters.
    
    ### How was this patch tested?
    Added UT.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Yes.
    
    Closes #57213 from sarutak/SPARK-57932-branch-4.2.
    
    Lead-authored-by: Kousuke Saruta <[email protected]>
    Co-authored-by: Eric Yang <[email protected]>
    Signed-off-by: Kousuke Saruta <[email protected]>
---
 .../catalyst/expressions/regexpExpressions.scala   | 17 +++++++-----
 .../expressions/RegexpExpressionsSuite.scala       | 31 ++++++++++++++++++++++
 2 files changed, 41 insertions(+), 7 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala
index 5ad360a54e8d..6740db92b6c8 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/regexpExpressions.scala
@@ -740,9 +740,9 @@ case class RegExpReplace(subject: Expression, regexp: 
Expression, rep: Expressio
     }
     val source = s.toString()
     val position = i.asInstanceOf[Int] - 1
-    if (position == 0 || position < source.length) {
+    if (position == 0 || position < source.codePointCount(0, source.length)) {
       val m = pattern.matcher(source)
-      m.region(position, source.length)
+      m.region(source.offsetByCodePoints(0, position), source.length)
       result.delete(0, result.length())
       while (m.find) {
         try {
@@ -795,9 +795,9 @@ case class RegExpReplace(subject: Expression, regexp: 
Expression, rep: Expressio
       }
       String $source = $subject.toString();
       int $position = $pos - 1;
-      if ($position == 0 || $position < $source.length()) {
+      if ($position == 0 || $position < $source.codePointCount(0, 
$source.length())) {
         $classNameStringBuilder $termResult = new $classNameStringBuilder();
-        $matcher.region($position, $source.length());
+        $matcher.region($source.offsetByCodePoints(0, $position), 
$source.length());
 
         while ($matcher.find()) {
           try {
@@ -1196,9 +1196,10 @@ case class RegExpInStr(subject: Expression, regexp: 
Expression, idx: Expression)
 
   override def nullSafeEval(s: Any, r: Any, i: Any): Any = {
     try {
-      val m = getLastMatcher(s, r)
+      val source = s.toString
+      val m = getLastMatcher(source, r)
       if (m.find) {
-        m.toMatchResult.start() + 1
+        source.codePointCount(0, m.toMatchResult.start()) + 1
       } else {
         0
       }
@@ -1212,6 +1213,7 @@ case class RegExpInStr(subject: Expression, regexp: 
Expression, idx: Expression)
 
   override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): 
ExprCode = {
     val matcher = ctx.freshName("matcher")
+    val source = ctx.freshName("source")
     val setEvNotNull = if (nullable) {
       s"${ev.isNull} = false;"
     } else {
@@ -1225,7 +1227,8 @@ case class RegExpInStr(subject: Expression, regexp: 
Expression, idx: Expression)
          |  ${RegExpUtils.initLastMatcherCode(ctx, subject, regexp, matcher, 
prettyName,
         collationId)}
          |  if ($matcher.find()) {
-         |    ${ev.value} = $matcher.toMatchResult().start() + 1;
+         |    String $source = $subject.toString();
+         |    ${ev.value} = $source.codePointCount(0, 
$matcher.toMatchResult().start()) + 1;
          |  } else {
          |    ${ev.value} = 0;
          |  }
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/RegexpExpressionsSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/RegexpExpressionsSuite.scala
index 9b5a8f2fce06..270480a63c0f 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/RegexpExpressionsSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/RegexpExpressionsSuite.scala
@@ -366,6 +366,26 @@ class RegexpExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
       RegExpReplace(Literal("\"quote"), Literal("\"quote"), 
Literal("\"quote")) :: Nil)
   }
 
+  test("SPARK-57932: regexp_replace position is a code-point position for 
supplementary chars") {
+    // scalastyle:off nonascii
+    checkEvaluation(
+      RegExpReplace(Literal("😀aXa"), Literal("a"), Literal("Z"), Literal(3)),
+      "😀aXZ")
+    checkEvaluation(
+      RegExpReplace(Literal("😀😀"), Literal("😀"),
+        Literal("Z"), Literal(3)),
+      "😀😀")
+    // Position beyond the string length: nothing is replaced.
+    checkEvaluation(
+      RegExpReplace(Literal("😀a"), Literal("a"), Literal("Z"), Literal(4)),
+      "😀a")
+    // Position at the last code-point.
+    checkEvaluation(
+      RegExpReplace(Literal("a😀b"), Literal("b"), Literal("Z"), Literal(3)),
+      "a😀Z")
+    // scalastyle:on nonascii
+  }
+
   test("SPARK-22570: RegExpReplace should not create a lot of global 
variables") {
     val ctx = new CodegenContext
     RegExpReplace(Literal("100"), Literal("(\\d+)"), 
Literal("num")).genCode(ctx)
@@ -627,6 +647,17 @@ class RegexpExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
       new RegExpInStr(Literal("\"quote"), Literal("\"quote")) :: Nil)
   }
 
+  test("SPARK-57932: regexp_instr returns a code-point position for 
supplementary characters") {
+    // scalastyle:off nonascii
+    checkEvaluation(RegExpInStr(Literal("😀ab"), Literal("ab"), Literal(0)), 2)
+    checkEvaluation(RegExpInStr(Literal("a😀b"), Literal("b"), Literal(0)), 3)
+    checkEvaluation(
+      RegExpInStr(Literal("😀😁xy"), Literal("xy"), Literal(0)), 3)
+    // A match made up entirely of supplementary characters.
+    checkEvaluation(RegExpInStr(Literal("😀😁😂"), Literal("😁"), Literal(0)), 2)
+    // scalastyle:on nonascii
+  }
+
   test("SPARK-39758: invalid regexp pattern") {
     val s = $"s".string.at(0)
     val p = $"p".string.at(1)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to