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