diff --git a/common/unsafe/src/main/java/org/apache/spark/sql/catalyst/util/CollationAwareUTF8String.java b/common/unsafe/src/main/java/org/apache/spark/sql/catalyst/util/CollationAwareUTF8String.java index 5e239212dfc66..18535357e3177 100644 --- a/common/unsafe/src/main/java/org/apache/spark/sql/catalyst/util/CollationAwareUTF8String.java +++ b/common/unsafe/src/main/java/org/apache/spark/sql/catalyst/util/CollationAwareUTF8String.java @@ -817,7 +817,7 @@ private static int lowercaseIndexOfSlow(final UTF8String target, final UTF8Strin * * @param target the string to be searched in * @param pattern the string to be searched for - * @param start the start position for searching (1-based, can be negative for backward search) + * @param start 1-based position; negative searches backward, zero returns {@code MATCH_NOT_FOUND} * @param occurrence which occurrence to return (must be >= 1) * @return the position of the {@code occurrence}-th occurrence of pattern in target * (0-based character index), or {@code MATCH_NOT_FOUND} if not found @@ -825,8 +825,8 @@ private static int lowercaseIndexOfSlow(final UTF8String target, final UTF8Strin public static int lowercaseIndexOf(final UTF8String target, final UTF8String pattern, final int start, final int occurrence) { assert occurrence > 0; - if (pattern.numBytes() == 0) return target.indexOfEmpty(start); if (start == 0) return MATCH_NOT_FOUND; + if (pattern.numBytes() == 0) return target.indexOfEmpty(start); if (target.isFullAscii() && pattern.isFullAscii()) { return target.toLowerCase().indexOf(pattern.toLowerCase(), start, occurrence); } @@ -919,7 +919,7 @@ public static int indexOf(final UTF8String target, final UTF8String pattern, * * @param target the string to be searched in * @param pattern the string to be searched for - * @param start the start position for searching (1-based, can be negative for backward search) + * @param start 1-based position; negative searches backward, zero returns {@code MATCH_NOT_FOUND} * @param occurrence which occurrence to return (must be >= 1) * @param collationId the ICU collation identifier * @return the position of the {@code occurrence}-th occurrence of pattern in target @@ -928,8 +928,10 @@ public static int indexOf(final UTF8String target, final UTF8String pattern, public static int indexOf(final UTF8String target, final UTF8String pattern, final int start, final int occurrence, final int collationId) { assert occurrence > 0; + // Reject zero start before the empty-pattern shortcut to preserve SQL instr semantics. + if (start == 0) return MATCH_NOT_FOUND; if (pattern.numBytes() == 0) return target.indexOfEmpty(start); - if (target.numBytes() == 0 || start == 0) return MATCH_NOT_FOUND; + if (target.numBytes() == 0) return MATCH_NOT_FOUND; String targetStr = target.toValidString(); String patternStr = pattern.toValidString(); diff --git a/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java b/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java index 5318cd8d11c29..01e28f7928a98 100644 --- a/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java +++ b/common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java @@ -1313,18 +1313,18 @@ public int indexOf(UTF8String v, int start) { * occurrence 1, 2 respectively). * * @param pattern the substring to search for - * @param start 1-based start position; if negative, search direction is reversed + * @param start 1-based position; negative searches backward, zero returns -1 * @param occurrence which occurrence to return (must be >= 1) * @return 0-based character index of the match, or -1 if not found */ public int indexOf(UTF8String pattern, int start, int occurrence) { assert occurrence > 0; - if (pattern.numBytes() == 0) { - return indexOfEmpty(start); - } if (start == 0) { return -1; } + if (pattern.numBytes() == 0) { + return indexOfEmpty(start); + } int charCount = 0; int byteIdx; diff --git a/common/unsafe/src/test/java/org/apache/spark/unsafe/types/CollationSupportSuite.java b/common/unsafe/src/test/java/org/apache/spark/unsafe/types/CollationSupportSuite.java index edd24671f1dc7..908de60074310 100644 --- a/common/unsafe/src/test/java/org/apache/spark/unsafe/types/CollationSupportSuite.java +++ b/common/unsafe/src/test/java/org/apache/spark/unsafe/types/CollationSupportSuite.java @@ -4047,6 +4047,19 @@ private void assertStringInstrWithOccurrence(String string, String substring, in assertEquals(expected, res); } + @Test + public void testStringInstrWithOccurrenceZeroStart() throws SparkException { + for (String collationName : testSupportedCollations) { + for (String string : new String[]{"", "abc", "\u4F60\u597D"}) { + for (String substring : new String[]{"", "a"}) { + for (int occurrence : new int[]{1, 2}) { + assertStringInstrWithOccurrence(string, substring, 0, occurrence, collationName, 0); + } + } + } + } + } + @Test public void testStringInstrWithOccurrence() throws SparkException { // Test start = 1 and occurrence = 1 (equivalent to StringInstr) diff --git a/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java b/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java index c871b785abb04..d6453daf8fb8b 100644 --- a/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java +++ b/common/unsafe/src/test/java/org/apache/spark/unsafe/types/UTF8StringSuite.java @@ -388,6 +388,10 @@ public void indexOf() { assertEquals(0, fromString("hello").indexOf(EMPTY_UTF8, 1, 1)); assertEquals(0, fromString("hello").indexOf(EMPTY_UTF8, 5, 1)); assertEquals(0, fromString("hello").indexOf(EMPTY_UTF8, -1, 1)); + assertEquals(-1, fromString("hello").indexOf(EMPTY_UTF8, 0, 1)); + assertEquals(-1, fromString("hello").indexOf(EMPTY_UTF8, 0, 2)); + assertEquals(-1, EMPTY_UTF8.indexOf(EMPTY_UTF8, 0, 1)); + assertEquals(-1, EMPTY_UTF8.indexOf(EMPTY_UTF8, 0, 2)); // Boundary cases assertEquals(0, fromString("x").indexOf(fromString("x"), 1, 1)); diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala index f3d479887ab27..364bc58f26ae0 100644 --- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/StringExpressionsSuite.scala @@ -1348,6 +1348,24 @@ class StringExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { // scalastyle:on } + test("INSTR with start = 0 returns 0 for empty substrings") { + for (collation <- Seq("UTF8_BINARY", "UTF8_LCASE", "UNICODE", "UNICODE_CI")) { + val stringType = StringType(collation) + val expression = StringInstrWithOccurrence( + BoundReference(0, stringType, nullable = false), + BoundReference(1, stringType, nullable = false), + BoundReference(2, IntegerType, nullable = false), + BoundReference(3, IntegerType, nullable = false)) + for { + string <- Seq("", "abc") + substring <- Seq("", "a") + occurrence <- Seq(1, 2) + } { + checkEvaluation(expression, 0, create_row(string, substring, 0, occurrence)) + } + } + } + test("StringInstrExpressionBuilder") { val seq1 = Seq(Literal("abcabc"), Literal("a"), Literal(-1), Literal(2)) val seq2 = Seq(Literal("abcabc"), Literal("a"), Literal(-1))