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

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


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

commit 8fabde78129109e82212c867e9d19548aef856e9
Author: Eric Yang <[email protected]>
AuthorDate: Fri Jul 10 09:05:09 2026 +0900

    [SPARK-57932][SQL] Fix regexp_instr and regexp_replace returning wrong 
results for supplementary characters
    
    ### What changes were proposed in this pull request?
    `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 #56998 from jiwen624/SPARK-regexp-instr-codepoint.
    
    Authored-by: Eric Yang <[email protected]>
    Signed-off-by: Kousuke Saruta <[email protected]>
---
 .../catalyst/expressions/regexpExpressions.scala   | 13 +++++----
 .../expressions/RegexpExpressionsSuite.scala       | 31 ++++++++++++++++++++++
 2 files changed, 39 insertions(+), 5 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 3bd0352f6f07..e99c6b2b29e7 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
@@ -1124,9 +1124,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
       }
@@ -1140,6 +1141,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 {
@@ -1153,7 +1155,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;
          |  }
@@ -1232,9 +1235,9 @@ object RegExpUtils {
       replacement: String,
       pos: Int): UTF8String = {
     val position = pos - 1
-    if (position == 0 || position < source.length) {
+    if (position == 0 || position < source.codePointCount(0, source.length)) {
       val matcher = pattern.matcher(source)
-      matcher.region(position, source.length)
+      matcher.region(source.offsetByCodePoints(0, position), source.length)
       val result = new JStringBuilder
       while (matcher.find()) {
         try {
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 19c8c8f341f0..0bf29553ea33 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
@@ -377,6 +377,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)
@@ -638,6 +658,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