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

zhenchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new b642769e16 [CALCITE-7042] Eliminate nested TRIM calls, exploiting the 
fact that TRIM is idempotent
b642769e16 is described below

commit b642769e1622d6953190e06321f67580606b6ba8
Author: Yu Xu <[email protected]>
AuthorDate: Mon May 26 10:02:43 2025 +0800

    [CALCITE-7042] Eliminate nested TRIM calls, exploiting the fact that TRIM 
is idempotent
---
 .../org/apache/calcite/rex/RexInterpreter.java     |  21 ++++
 .../java/org/apache/calcite/rex/RexSimplify.java   |  41 +++++++
 .../java/org/apache/calcite/util/NlsString.java    |  36 ++++++
 .../calcite/test/RexImplicationCheckerTest.java    | 123 +++++++++++++++++++++
 4 files changed, 221 insertions(+)

diff --git a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java 
b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
index f40d687d44..38d43c6efb 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
@@ -230,6 +230,8 @@ private Comparable getOrUnbound(RexNode e) {
     case CEIL:
     case FLOOR:
       return ceil(call, values);
+    case TRIM:
+      return trim(call, values);
     case EXTRACT:
       return extract(values);
     case LIKE:
@@ -339,6 +341,25 @@ private static Comparable coalesce(List<Comparable> 
values) {
     return N;
   }
 
+  private static Comparable trim(RexNode call, List<Comparable> values) {
+    if (containsNull(values)) {
+      return N;
+    }
+    NlsString trimType = (NlsString) values.get(0);
+    NlsString trimed = (NlsString) values.get(1);
+    NlsString trimString = (NlsString) values.get(2);
+    switch (trimType.getValue()) {
+    case "BOTH":
+      return trimString.trim(trimed.getValue());
+    case "LEADING":
+      return trimString.ltrim(trimed.getValue());
+    case "TRAILING":
+      return trimString.rtrim(trimed.getValue());
+    default:
+      throw unbound(call);
+    }
+  }
+
   private static Comparable ceil(RexCall call, List<Comparable> values) {
     if (values.get(0) == N) {
       return N;
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java 
b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
index 8739274f8e..ed1b019d58 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -294,6 +294,8 @@ RexNode simplify(RexNode e, RexUnknownAs unknownAs) {
     case CEIL:
     case FLOOR:
       return simplifyCeilFloor((RexCall) e);
+    case TRIM:
+      return simplifyTrim((RexCall) e);
     case IS_NULL:
     case IS_NOT_NULL:
     case IS_TRUE:
@@ -2394,6 +2396,45 @@ private RexNode simplifyCeilFloor(RexCall e) {
         ImmutableList.of(operand, e.getOperands().get(1)));
   }
 
+  /** Simplify TRIM function by eliminating nested duplication.
+   *
+   * <p>Examples:
+   * <ul>
+   *
+   * <li>{@code trim(trim(' aa '))} returns {@code trim(' aa ')}
+   *
+   * <li>{@code trim(BOTH ' ' from trim(BOTH ' ' from ' aa '))}
+   * returns {@code trim(BOTH ' ' from ' aa ')}
+   *
+   * <li>{@code trim(LEADING 'a' from trim(BOTH ' ' from ' aa '))} does not 
change
+   *
+   * </ul>
+   */
+  private RexNode simplifyTrim(RexCall e) {
+    if (e.getOperands().size() != 3) {
+      return e;
+    }
+
+    RexNode trimType = simplify(e.operands.get(0));
+    RexNode trimed = simplify(e.operands.get(1));
+    if (e.getOperands().get(2) instanceof RexCall) {
+      RexCall childNode = (RexCall) e.getOperands().get(2);
+      // only strings with the same trim method and deduplication will be 
eliminated.
+      if (childNode.getKind() == SqlKind.TRIM
+          && trimType.equals(simplify(childNode.operands.get(0)))
+          && trimed.equals(simplify(childNode.operands.get(1)))) {
+        return simplifyTrim(childNode);
+      }
+    }
+
+    ArrayList<RexNode> rexNodes = new ArrayList<>();
+    rexNodes.add(trimType);
+    rexNodes.add(trimed);
+    rexNodes.add(simplify(e.operands.get(2)));
+    RexNode rexNode = rexBuilder.makeCall(e.getType(), e.getOperator(), 
rexNodes);
+    return rexNode;
+  }
+
   /** Method that returns whether we can rollup from inner time unit
    * to outer time unit. */
   private static boolean canRollUp(TimeUnit outer, TimeUnit inner) {
diff --git a/core/src/main/java/org/apache/calcite/util/NlsString.java 
b/core/src/main/java/org/apache/calcite/util/NlsString.java
index e4e9d8e632..b98984bf53 100644
--- a/core/src/main/java/org/apache/calcite/util/NlsString.java
+++ b/core/src/main/java/org/apache/calcite/util/NlsString.java
@@ -227,6 +227,42 @@ public NlsString rtrim() {
     return this;
   }
 
+  /**
+   * Returns a string the same as this but with spaces trimmed from the
+   * left and right.
+   */
+  public NlsString trim(String trimed) {
+    String trimmed = SqlFunctions.trim(true, true, trimed, getValue());
+    if (!trimmed.equals(getValue())) {
+      return new NlsString(trimmed, charsetName, collation);
+    }
+    return this;
+  }
+
+  /**
+   * Returns a string the same as this but with spaces trimmed from the
+   * left.
+   */
+  public NlsString ltrim(String trimed) {
+    String trimmed = SqlFunctions.trim(true, false, trimed, getValue());
+    if (!trimmed.equals(getValue())) {
+      return new NlsString(trimmed, charsetName, collation);
+    }
+    return this;
+  }
+
+  /**
+   * Returns a string the same as this but with spaces trimmed from the
+   * right.
+   */
+  public NlsString rtrim(String trimed) {
+    String trimmed = SqlFunctions.trim(false, true, trimed, getValue());
+    if (!trimmed.equals(getValue())) {
+      return new NlsString(trimmed, charsetName, collation);
+    }
+    return this;
+  }
+
   /**
    * Returns a string the same as this but with spaces trimmed from the
    * right.  The result is never shorter than minResultSize.
diff --git 
a/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java 
b/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java
index 5eebc5410c..042dcae1f0 100644
--- a/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java
@@ -25,6 +25,7 @@
 import org.apache.calcite.rex.RexUnknownAs;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.util.DateString;
 import org.apache.calcite.util.TimeString;
 import org.apache.calcite.util.TimestampString;
@@ -360,6 +361,128 @@ public class RexImplicationCheckerTest {
         hasToString("2014"));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7042";>[CALCITE-7042]
+   * Eliminate nested TRIM calls, exploiting the fact that TRIM is 
idempotent</a>. */
+  @Test void testSimplifyIdempotentFunctions() {
+    final Fixture f = new Fixture();
+    RexLiteral trimBoth = f.rexBuilder.makeLiteral("BOTH");
+    RexLiteral trimed = f.rexBuilder.makeLiteral("a");
+    RexLiteral trimString = f.rexBuilder.makeLiteral("bb");
+
+    String[] trimWays = {"BOTH", "LEADING", "TRAILING"};
+    for (String trim : trimWays) {
+      // trim way and string are the same
+      RexCall innerTrimCall =
+          (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM,
+              f.rexBuilder.makeLiteral(trim), trimed, trimString);
+      RexCall outerTrimCall =
+          (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM,
+              f.rexBuilder.makeLiteral(trim), trimed, innerTrimCall);
+      RexCall trimSimplifiedCall =
+          (RexCall) f.simplify.simplifyPreservingType(outerTrimCall,
+              RexUnknownAs.UNKNOWN, true);
+
+      // after simplify trimSimplifiedCall is equal to innerTrimCall
+      assertThat(trimSimplifiedCall.getKind(), is(SqlKind.TRIM));
+      assertThat(((RexLiteral) trimSimplifiedCall.getOperands().get(1))
+              .getValue(),
+          is(((RexLiteral) innerTrimCall.getOperands().get(1)).getValue()));
+      assertThat(((RexLiteral) trimSimplifiedCall.getOperands().get(2))
+              .getValue(),
+          is(((RexLiteral) innerTrimCall.getOperands().get(2)).getValue()));
+    }
+
+    // trim string are expression
+    RexCall innerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth, 
trimed, trimString);
+    RexCall outerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth, 
trimed, innerTrimCall);
+    RexCall trimSimplified =
+        (RexCall) f.simplify.simplifyPreservingType(outerTrimCall,
+            RexUnknownAs.UNKNOWN, true);
+    final RelDataType integer =
+        f.rexBuilder.getTypeFactory().createSqlType(SqlTypeName.INTEGER);
+    final RexNode lengthArg = f.rexBuilder.makeLiteral(1, integer, true);
+    RexNode expressionTrim =
+        f.rexBuilder.makeCall(SqlStdOperatorTable.SUBSTRING, trimed, 
lengthArg);
+    innerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth,
+            expressionTrim, trimString);
+    outerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth,
+            expressionTrim, innerTrimCall);
+    trimSimplified =
+        (RexCall) f.simplify.simplifyPreservingType(outerTrimCall,
+            RexUnknownAs.UNKNOWN, true);
+    // after simplify trimSimplifiedCall is equal to innerTrimCall
+    assertThat(trimSimplified.getOperands().get(1),
+        is(innerTrimCall.getOperands().get(1)));
+    assertThat(trimSimplified.getOperands().get(2),
+        is(innerTrimCall.getOperands().get(2)));
+
+    // Negative test of trim way is not the same
+    RexLiteral trimLeft = f.rexBuilder.makeLiteral("LEADING");
+    innerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth, 
trimed, trimString);
+    RexCall outerLeftTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimLeft, 
trimed, innerTrimCall);
+    trimSimplified =
+        (RexCall) f.simplify.simplifyPreservingType(outerLeftTrimCall,
+            RexUnknownAs.UNKNOWN, true);
+
+    // after simplify trimSimplifiedCall is not equal to innerTrimCall
+    assertThat(trimSimplified.getKind(), is(SqlKind.TRIM));
+    assertThat(((RexLiteral) trimSimplified.getOperands().get(1))
+            .getValue(),
+        is(((RexLiteral) innerTrimCall.getOperands().get(1)).getValue()));
+    assertThat(trimSimplified.getOperands().get(2),
+        is(innerTrimCall));
+
+    // Negative test of trimed string is null
+    RexLiteral trimNull = f.rexBuilder.makeNullLiteral(trimString.getType());
+    innerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth, 
trimNull, trimString);
+    RexCall outerNullTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimLeft, 
trimed, innerTrimCall);
+    RexNode trimSimplifiedNullCall =
+        f.simplify.simplifyPreservingType(outerNullTrimCall,
+            RexUnknownAs.UNKNOWN, true);
+    // after simplify trimSimplifiedCall is equal to null
+    assertThat(trimSimplifiedNullCall, hasToString("null:VARCHAR(2)"));
+
+    // Negative test of string is null
+    trimNull = f.rexBuilder.makeNullLiteral(trimString.getType());
+    innerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth, 
trimed, trimNull);
+    outerNullTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimLeft, 
trimed, innerTrimCall);
+    trimSimplifiedNullCall =
+        f.simplify.simplifyPreservingType(outerNullTrimCall,
+            RexUnknownAs.UNKNOWN, true);
+    // after simplify trimSimplifiedCall is equal to null
+    assertThat(trimSimplifiedNullCall, hasToString("null:VARCHAR(2)"));
+
+    // simplifyTrim supports recursive simplification
+    RelDataType varcharType =
+        f.typeFactory.createSqlType(SqlTypeName.VARCHAR);
+    // castCall is CAST(CAST(1111):VARCHAR NOT NULL):VARCHAR NOT NULL
+    final RexNode castCall =
+        f.cast(varcharType,
+            f.cast(varcharType, f.literal(1111)));
+    innerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth, 
trimed, castCall);
+    // outerTrimCall is
+    // TRIM('BOTH', 'a', TRIM('BOTH', 'a', CAST(CAST(1111):VARCHAR NOT 
NULL):VARCHAR NOT NULL))
+    outerTrimCall =
+        (RexCall) f.rexBuilder.makeCall(SqlStdOperatorTable.TRIM, trimBoth, 
trimed, innerTrimCall);
+    trimSimplified =
+        (RexCall) f.simplify.simplifyPreservingType(outerTrimCall,
+            RexUnknownAs.UNKNOWN, true);
+    assertThat(trimSimplified,
+        hasToString("TRIM('BOTH', 'a', '1111':VARCHAR)"));
+  }
+
   /** Test case for simplifier of ceil/floor. */
   @Test void testSimplifyCeilFloor() {
     // We can add more time units here once they are supported in

Reply via email to