pratham76 commented on code in PR #59213:
URL: https://github.com/apache/spark/pull/59213#discussion_r4188005782


##########
sql/core/src/test/scala/org/apache/spark/sql/execution/benchmark/StringFunctionsBenchmark.scala:
##########
@@ -53,6 +53,18 @@ object StringFunctionsBenchmark extends SqlBasedBenchmark {
         doRegexpReplaceBenchmark()
       }
       benchmark.run()
+
+      val splitBenchmark = new Benchmark("split", N, output = output)
+      splitBenchmark.addCase("split('*-*', '-')", M) { _ =>
+        df.selectExpr("split(subject, '-')").noop()
+      }
+      splitBenchmark.addCase("split('*-*', '[-]')", M) { _ =>
+        df.selectExpr("split(subject, '[-]')").noop()
+      }
+      splitBenchmark.addCase("split('*-*' COLLATE UTF8_LCASE, '-')", M) { _ =>
+        df.selectExpr("split(subject COLLATE UTF8_LCASE, '-')").noop()
+      }
+      splitBenchmark.run()

Review Comment:
   This PR bundles three independent things: (1) the core Pattern cache fix, 
(2) replicating JDK's single-char fast-path heuristic (splitsWithoutRegex, 
UTF8String.java), and (3) this new benchmark. Could we split the fast-path 
replication and the benchmark into follow-ups so the core cache fix is easier 
to review/backport on its own?"



##########
common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java:
##########
@@ -1720,25 +1742,54 @@ public UTF8String[] splitSQL(UTF8String delimiter, int 
limit) {
       // as the separator to split string. Java String's split, however, only 
accept
       // regex as the pattern to split, thus we can quote the delimiter to 
escape special
       // characters in the string.
-      return split(Pattern.quote(delimiter.toString()), limit);
+      return split(Pattern.quote(delimiter.toString()), null, limit);
     }
   }
 
-  private UTF8String[] split(String delimiter, int limit) {
+  // `compiled` is `delimiter` already compiled, or null to let String.split 
compile it.
+  private UTF8String[] split(String delimiter, Pattern compiled, int limit) {
     // Java String's split method supports "ignore empty string" behavior when 
the limit is 0
     // whereas other languages do not. To avoid this java specific behavior, 
we fall back to
     // -1 when the limit is 0.
     if (limit == 0) {
       limit = -1;
     }
-    String[] splits = toString().split(delimiter, limit);
+    // String.split splits around a single-char delimiter with indexOf rather 
than a regex, which
+    // beats matching even a pre-compiled Pattern, so only use `compiled` 
where it would not.
+    String[] splits =
+      compiled == null || (compiled.flags() == 0 && 
splitsWithoutRegex(delimiter)) ?
+        toString().split(delimiter, limit) : compiled.split(toString(), limit);

Review Comment:
   compiled.flags() is recomputed on every call. It's cheap (int field read) 
but since we're already caching the Pattern object one level up in StringSplit, 
worth a comment noting this is intentionally re-checked per row rather than 
cached, so a future reader doesn't assume it's an oversight.



##########
common/unsafe/src/main/java/org/apache/spark/unsafe/types/UTF8String.java:
##########
@@ -1720,25 +1742,54 @@ public UTF8String[] splitSQL(UTF8String delimiter, int 
limit) {
       // as the separator to split string. Java String's split, however, only 
accept
       // regex as the pattern to split, thus we can quote the delimiter to 
escape special
       // characters in the string.
-      return split(Pattern.quote(delimiter.toString()), limit);
+      return split(Pattern.quote(delimiter.toString()), null, limit);
     }
   }
 
-  private UTF8String[] split(String delimiter, int limit) {
+  // `compiled` is `delimiter` already compiled, or null to let String.split 
compile it.
+  private UTF8String[] split(String delimiter, Pattern compiled, int limit) {
     // Java String's split method supports "ignore empty string" behavior when 
the limit is 0
     // whereas other languages do not. To avoid this java specific behavior, 
we fall back to
     // -1 when the limit is 0.
     if (limit == 0) {
       limit = -1;
     }
-    String[] splits = toString().split(delimiter, limit);
+    // String.split splits around a single-char delimiter with indexOf rather 
than a regex, which
+    // beats matching even a pre-compiled Pattern, so only use `compiled` 
where it would not.
+    String[] splits =
+      compiled == null || (compiled.flags() == 0 && 
splitsWithoutRegex(delimiter)) ?
+        toString().split(delimiter, limit) : compiled.split(toString(), limit);
     UTF8String[] res = new UTF8String[splits.length];
     for (int i = 0; i < res.length; i++) {
       res[i] = fromString(splits[i]);
     }
     return res;
   }
 
+  /**
+   * Mirrors the check String.split uses to split around `regex` with indexOf 
instead of compiling
+   * it: a single char that is not a regex metacharacter, or a backslash 
followed by a char that
+   * is not an ASCII letter or digit, and in both cases not a surrogate. This 
only picks the faster
+   * of two equivalent calls: otherwise String.split is 
Pattern.compile(regex).split(this, limit).
+   */
+  private static boolean splitsWithoutRegex(String regex) {

Review Comment:
   This duplicates JDK's internal String.split single-char fast-path detection 
(metacharacter set, backslash-escape check, surrogate guard) by hand. If a 
future JDK changes that heuristic, this silently diverges (perf-only risk, not 
correctness, since Pattern.split and String.split are always equivalent — but 
still a maintenance footgun). Could use a one-line comment pointing at the JDK 
source/version this was copied from, so it's easier to re-check on JDK upgrades.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/RegexpExpressionsSuite.scala:
##########
@@ -627,6 +629,26 @@ class RegexpExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
       StringSplit(Literal("\"quote"), Literal("\"quote"), Literal(-1)) :: Nil)
   }
 
+  test("SPARK-59961: split refreshes its cached pattern when the regex 
changes") {

Review Comment:
   Good test for cache-invalidation correctness, but it only exercises the 
default collation + default legacySplitTruncate=false path. Could we add a case 
covering legacySplitTruncate=true and non-UTF8_BINARY collation together with a 
regex change.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to