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

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new a7cfea0b824 Upgrade Commons Lang to 3.21.0 and discard trailing empty 
split fields (#19747)
a7cfea0b824 is described below

commit a7cfea0b824a2c30a0faf1e0a80a42d720e1a12a
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Sun Oct 4 21:23:35 2026 -0700

    Upgrade Commons Lang to 3.21.0 and discard trailing empty split fields 
(#19747)
---
 .../common/function/scalar/StringFunctions.java    | 72 +++++------------
 .../function/scalar/StringFunctionsTest.java       | 89 ++++++++++++++++------
 pom.xml                                            |  2 +-
 3 files changed, 86 insertions(+), 77 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
index da33990b79b..bbb73a54e64 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
@@ -493,7 +493,9 @@ public class StringFunctions {
     return suffixArr;
   }
 
-  /// TODO: Revisit if index should be one-based (both Presto and Postgres use 
one-based index, which starts with 1)
+  /// Empty fields from leading, consecutive, and trailing delimiters are 
discarded.
+  /// TODO: Revisit whether to use one-based indexes and preserve empty fields 
from leading, consecutive, and trailing
+  /// delimiters, as Presto and Postgres do.
   /// @param input the input String to be split into parts.
   /// @param delimiter the specified delimiter to split the input string.
   /// @param index we allow negative value for index which indicates the index 
from the end.
@@ -517,8 +519,8 @@ public class StringFunctions {
     while (start < len && input.startsWith(delimiter, start)) {
       start += delimLen;
     }
-    // Guard against Integer.MIN_VALUE since negating it overflows (remains 
negative)
-    if (index == Integer.MIN_VALUE) {
+    // No fields remain if the input contains only delimiters. Negating 
Integer.MIN_VALUE would overflow.
+    if (start == len || index == Integer.MIN_VALUE) {
       return "null";
     }
     // optimization for negative index with single-char delimiter since common 
case
@@ -532,7 +534,7 @@ public class StringFunctions {
     if (adjustedIndex < 0) {
       int totalFields = 0;
       int pos = start;
-      while (pos <= len) {
+      while (pos < len) {
         totalFields++;
         int end = input.indexOf(delimiter, pos);
         if (end == -1) {
@@ -561,34 +563,23 @@ public class StringFunctions {
       }
     }
 
+    if (start == len) {
+      return "null";
+    }
     int end = input.indexOf(delimiter, start);
     return end == -1 ? input.substring(start) : input.substring(start, end);
   }
 
   private static String splitPartNegativeIdxSingleCharDelim(
       String input, char delimiter, int index, int len, int start) {
-    // input is empty or contains only delimiters
-    if (start == len) {
-      return index == 1 ? "" : "null";
-    }
-
-    // scan backwards and handle trailing delimiters
+    // Scan backwards past trailing delimiters without counting empty fields.
     int end = len;
     while (end > start && input.charAt(end - 1) == delimiter) {
       end--;
     }
 
-    // handle trailing delimiters
-    int resultIdx = index;
-    if (end < len) {
-      if (index == 1) {
-        return "";
-      }
-      resultIdx--;
-    }
-
     int curEnd = end;
-    for (int i = 1; i <= resultIdx; i++) {
+    for (int i = 1; i <= index; i++) {
       // handle left out of bound index
       if (curEnd <= start) {
         return "null";
@@ -600,7 +591,7 @@ public class StringFunctions {
         curStart--;
       }
 
-      if (i == resultIdx) {
+      if (i == index) {
         return input.substring(curStart + 1, curEnd);
       }
 
@@ -628,8 +619,8 @@ public class StringFunctions {
   /// Avoids allocating the full String array by scanning the input directly.
   ///
   /// Replicates the semantics of [StringUtils#splitByWholeSeparator(String, 
String, int)]:
-  /// leading separators are stripped, consecutive separators in the middle 
are collapsed,
-  /// and trailing separators produce one empty trailing token.
+  /// empty fields from leading, consecutive, and trailing separators are 
discarded.
+  /// Once the limit is reached, the last field contains the unsplit 
remainder, including any trailing separators.
   ///
   /// @param input the input String to be split into parts.
   /// @param delimiter the specified delimiter to split the input string.
@@ -674,8 +665,8 @@ public class StringFunctions {
   }
 
   /// Counts the number of fields produced by splitting input with the given 
delimiter and limit,
-  /// following splitByWholeSeparator semantics (leading seps stripped, 
consecutive collapsed,
-  /// trailing seps produce one empty field). Does not allocate any String 
objects.
+  /// following splitByWholeSeparator semantics (empty fields discarded, final 
field contains the unsplit remainder
+  /// when the limit is reached). Does not allocate any String objects.
   private static int countFieldsLimited(String input, String delimiter, int 
effectiveLimit,
       int inputLen, int delimLen) {
     int pos = 0;
@@ -683,11 +674,6 @@ public class StringFunctions {
     while (pos < inputLen && input.startsWith(delimiter, pos)) {
       pos += delimLen;
     }
-    if (pos >= inputLen) {
-      // Entire string is separators — produces a single empty trailing token
-      return 1;
-    }
-
     int totalFields = 0;
     while (pos < inputLen) {
       totalFields++;
@@ -706,18 +692,13 @@ public class StringFunctions {
       while (pos < inputLen && input.startsWith(delimiter, pos)) {
         pos += delimLen;
       }
-      // If we've consumed to end after delimiters, there's a trailing empty 
field
-      if (pos >= inputLen) {
-        totalFields++;
-        break;
-      }
     }
     return totalFields;
   }
 
   /// Extracts the field at the given positive index by scanning forward 
through the input.
-  /// Follows splitByWholeSeparator semantics: leading separators stripped, 
consecutive collapsed,
-  /// trailing separators produce one empty token. With limit, the last field 
gets the remainder.
+  /// Follows splitByWholeSeparator semantics: empty fields discarded.
+  /// With limit, the last field gets the unsplit remainder, including any 
trailing separators.
   private static String splitPartLimitedForward(String input, String 
delimiter, int effectiveLimit, int index,
       int inputLen, int delimLen) {
     int pos = 0;
@@ -725,13 +706,8 @@ public class StringFunctions {
     while (pos < inputLen && input.startsWith(delimiter, pos)) {
       pos += delimLen;
     }
-    if (pos >= inputLen) {
-      // Entire string is separators — single empty trailing token at index 0
-      return index == 0 ? "" : "null";
-    }
-
     int fieldCount = 0;
-    while (pos <= inputLen) {
+    while (pos < inputLen) {
       // Check if this is the last field due to limit
       if (fieldCount + 1 >= effectiveLimit) {
         // Limit reached — remainder from pos to end is the last field
@@ -745,8 +721,6 @@ public class StringFunctions {
         if (fieldCount == index) {
           return input.substring(pos);
         }
-        // Check if input ends with delimiter characters that were already 
consumed
-        // (this case is handled by the trailing-delimiter logic below)
         return "null";
       }
 
@@ -761,14 +735,6 @@ public class StringFunctions {
         pos += delimLen;
       }
       fieldCount++;
-
-      // If we've consumed to the end after delimiters, there's a trailing 
empty field
-      if (pos >= inputLen) {
-        if (fieldCount == index) {
-          return "";
-        }
-        return "null";
-      }
     }
     return "null";
   }
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
index 56bed3f22ad..ed6f3359df1 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
@@ -46,7 +46,7 @@ public class StringFunctionsTest {
         {"org.apache.pinot.common.function", ".", 4, 5, "function", 
"function"},
         {"org.apache.pinot.common.function", ".", 5, 6, "null", "null"},
         {"org.apache.pinot.common.function", ".", 3, 3, "common", "null"},
-        {"+++++", "+", 0, 100, "", ""},
+        {"+++++", "+", 0, 100, "null", "null"},
         {"+++++", "+", 1, 100, "null", "null"},
         {"+++++org++apache++", "", 1, 100, "null", "null"},
         {"+++++org++apache++", "", 0, 100, "+++++org++apache++", 
"+++++org++apache++"},
@@ -70,7 +70,7 @@ public class StringFunctionsTest {
         {"org.apache.pinot.common.function", ".", -1, 6, "function", 
"function"},
         {"org.apache.pinot.common.function", ".", -5, 6, "org", "org"},
         {"org.apache.pinot.common.function", ".", -6, 6, "null", "null"},
-        {"+++++", "+", -1, 100, "", ""},
+        {"+++++", "+", -1, 100, "null", "null"},
         {"+++++", "+", -2, 100, "null", "null"},
 
         // Empty delimiter: index=-1 returns input, other negative indices 
return "null"
@@ -84,34 +84,34 @@ public class StringFunctionsTest {
         {"abc", ".", -2, 100, "null", "null"},
 
         // Input equals delimiter
-        {".", ".", 0, 100, "", ""},
+        {".", ".", 0, 100, "null", "null"},
         {".", ".", 1, 100, "null", "null"},
-        {".", ".", -1, 100, "", ""},
+        {".", ".", -1, 100, "null", "null"},
 
         // Trailing delimiters with content (single-char): exercises 
splitPartNegativeIdxSingleCharDelim
-        // trailing-delimiter handling (resultIdx decrement, empty trailing 
field)
+        // without counting empty trailing fields
         {"org++apache++", "+", 0, 100, "org", "org"},
         {"org++apache++", "+", 1, 100, "apache", "apache"},
-        {"org++apache++", "+", 2, 100, "", ""},
+        {"org++apache++", "+", 2, 100, "null", "null"},
         {"org++apache++", "+", 3, 100, "null", "null"},
-        {"org++apache++", "+", -1, 100, "", ""},
-        {"org++apache++", "+", -2, 100, "apache", "apache"},
-        {"org++apache++", "+", -3, 100, "org", "org"},
+        {"org++apache++", "+", -1, 100, "apache", "apache"},
+        {"org++apache++", "+", -2, 100, "org", "org"},
+        {"org++apache++", "+", -3, 100, "null", "null"},
         {"org++apache++", "+", -4, 100, "null", "null"},
 
         // Leading AND trailing delimiters (single-char): exercises backward 
scan with
-        // both leading delimiter skip and trailing delimiter adjustment
+        // both leading and trailing delimiters skipped
         {"++org++apache++", "+", 0, 100, "org", "org"},
         {"++org++apache++", "+", 1, 100, "apache", "apache"},
-        {"++org++apache++", "+", -1, 100, "", ""},
-        {"++org++apache++", "+", -2, 100, "apache", "apache"},
-        {"++org++apache++", "+", -3, 100, "org", "org"},
+        {"++org++apache++", "+", -1, 100, "apache", "apache"},
+        {"++org++apache++", "+", -2, 100, "org", "org"},
+        {"++org++apache++", "+", -3, 100, "null", "null"},
         {"++org++apache++", "+", -4, 100, "null", "null"},
 
         // Single field surrounded by delimiters
         {"++abc++", "+", 0, 100, "abc", "abc"},
-        {"++abc++", "+", -1, 100, "", ""},
-        {"++abc++", "+", -2, 100, "abc", "abc"},
+        {"++abc++", "+", -1, 100, "abc", "abc"},
+        {"++abc++", "+", -2, 100, "null", "null"},
         {"++abc++", "+", -3, 100, "null", "null"},
 
         // Multi-char delimiter: exercises forward scan and multi-char 
negative index
@@ -136,15 +136,41 @@ public class StringFunctionsTest {
         {"::::org::::apache", "::", -3, 100, "null", "null"},
 
         // Multi-char delimiter with leading AND trailing delimiters: 
exercises the
-        // trailing empty field in the multi-char totalFields counting path
+        // multi-char totalFields counting path without empty trailing fields
         {"::org::apache::", "::", 0, 100, "org", "org"},
         {"::org::apache::", "::", 1, 100, "apache", "apache"},
-        {"::org::apache::", "::", 2, 100, "", ""},
-        {"::org::apache::", "::", -1, 100, "", ""},
-        {"::org::apache::", "::", -2, 100, "apache", "apache"},
-        {"::org::apache::", "::", -3, 100, "org", "org"},
+        {"::org::apache::", "::", 2, 100, "null", "null"},
+        {"::org::apache::", "::", -1, 100, "apache", "apache"},
+        {"::org::apache::", "::", -2, 100, "org", "org"},
+        {"::org::apache::", "::", -3, 100, "null", "null"},
         {"::org::apache::", "::", -4, 100, "null", "null"},
 
+        // Trailing delimiters are discarded unless they belong to the unsplit 
remainder at the limit.
+        {"a,b,", ",", 1, 1, "b", "null"},
+        {"a,b,", ",", -1, 1, "b", "a,b,"},
+        {"a,b,", ",", 1, 2, "b", "b,"},
+        {"a,b,", ",", -1, 2, "b", "b,"},
+        {"a,b,", ",", 2, 2, "null", "null"},
+        {"a,b,", ",", -2, 2, "a", "a"},
+        {"a,b,", ",", -1, 3, "b", "b"},
+        {"a,b,", ",", 2, 3, "null", "null"},
+        {",,a,,b,,", ",", -1, 0, "b", "b"},
+        {",,a,,b,,", ",", -1, -1, "b", "b"},
+        {",,a,,b,,", ",", -1, 2, "b", "b,,"},
+        {"::a::::b::::", "::", 1, 2, "b", "b::::"},
+        {"::a::::b::::", "::", -1, 2, "b", "b::::"},
+        {"::a::::b::::", "::", -1, 3, "b", "b"},
+        {"::a::::b::::", "::", -2, 3, "a", "a"},
+        {"::a::::b::::", "::", 2, 3, "null", "null"},
+        {",,,", ",", 0, 1, "null", "null"},
+        {",,,", ",", -1, 1, "null", "null"},
+        {"::::", "::", 0, 1, "null", "null"},
+        {"::::", "::", -1, 1, "null", "null"},
+        // A suffix that is only part of a multi-character delimiter is a 
non-empty field.
+        {"aaaaa", "aa", 0, 100, "a", "a"},
+        {"aaaaa", "aa", -1, 100, "a", "a"},
+        {"ababa", "aba", -1, 100, "ba", "ba"},
+
         // Empty input with non-empty delimiter
         {"", ".", 0, 100, "null", "null"},
         {"", ".", -1, 100, "null", "null"},
@@ -155,6 +181,7 @@ public class StringFunctionsTest {
 
         // Integer.MIN_VALUE: negating it overflows (remains negative), guard 
must return "null"
         {"org.apache.pinot", ".", Integer.MIN_VALUE, 100, "null", "null"},
+        {"a,b,", ",", Integer.MAX_VALUE, 2, "null", "null"},
     };
   }
 
@@ -304,11 +331,21 @@ public class StringFunctionsTest {
     assertEquals(StringFunctions.splitPart(input, delimiter, limit, index), 
expectedTokenWithLimitCounts);
   }
 
+  @Test
+  public void testSplitDiscardsEmptyFields() {
+    assertEquals(StringFunctions.split(",,a,,b,,", ","), new String[]{"a", 
"b"});
+    assertEquals(StringFunctions.split("::a::::b::::", "::"), new 
String[]{"a", "b"});
+    assertEquals(StringFunctions.split(",,,", ","), new String[0]);
+    assertEquals(StringFunctions.split("::::", "::"), new String[0]);
+    assertEquals(StringFunctions.split("a,b,", ",", 2), new String[]{"a", 
"b,"});
+    assertEquals(StringFunctions.split("a,b,", ",", 3), new String[]{"a", 
"b"});
+  }
+
   @Test
   public void testSplitPartRandomized() {
-    String chars = "abcdefg.,:;+-_/";
-    String[] delimiters = {".", ",", ":", "::", "++", "ab", "///"};
-    Random random = new Random();
+    String chars = "abcdefg.,:;+-_/ \t";
+    String[] delimiters = {".", ",", ":", "::", "++", "ab", "///", "", " "};
+    Random random = new Random(0);
     int numIterations = 10_000;
 
     for (int iter = 0; iter < numIterations; iter++) {
@@ -326,6 +363,12 @@ public class StringFunctionsTest {
       String actual = StringFunctions.splitPart(input, delimiter, index);
       assertEquals(actual, expected,
           String.format("Mismatch for input='%s', delimiter='%s', index=%d", 
input, delimiter, index));
+
+      int limit = random.nextInt(8) - 2;
+      String expectedWithLimit = StringFunctions.splitPartArrayBased(
+          StringUtils.splitByWholeSeparator(input, delimiter, limit), index);
+      assertEquals(StringFunctions.splitPart(input, delimiter, limit, index), 
expectedWithLimit,
+          String.format("Mismatch for input='%s', delimiter='%s', limit=%d, 
index=%d", input, delimiter, limit, index));
     }
   }
 
diff --git a/pom.xml b/pom.xml
index a2b7e0adf73..3aa2cd22432 100644
--- a/pom.xml
+++ b/pom.xml
@@ -263,7 +263,7 @@
     <flink.version>2.3.0</flink.version>
 
     <!-- Apache Commons Libraries -->
-    <commons-lang3.version>3.20.0</commons-lang3.version>
+    <commons-lang3.version>3.21.0</commons-lang3.version>
     <commons-collections4.version>4.6.0</commons-collections4.version>
     <commons-text.version>1.15.0</commons-text.version>
     <commons-compress.version>1.28.0</commons-compress.version>


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

Reply via email to