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

rubenada 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 95101d8255 [CALCITE-7722] RexSimplify IS [NOT] NULL on a safe operator 
with Strong policy ANY and unsafe operands can be further simplified
95101d8255 is described below

commit 95101d8255cad4a0e92f25ec00797bb16338cfb3
Author: Ruben Quesada Lopez <[email protected]>
AuthorDate: Mon Aug 17 15:21:37 2026 +0100

    [CALCITE-7722] RexSimplify IS [NOT] NULL on a safe operator with Strong 
policy ANY and unsafe operands can be further simplified
---
 .../java/org/apache/calcite/rex/RexSimplify.java   |  74 ++++++++---
 .../apache/calcite/rex/RexProgramBuilderBase.java  |  78 +++++++++++
 .../org/apache/calcite/rex/RexProgramTest.java     | 146 +++++++++++++++++++++
 3 files changed, 282 insertions(+), 16 deletions(-)

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 fe9f7b5ca0..98fbbb781b 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -1200,15 +1200,21 @@ private RexNode simplifyIs(RexCall call, RexUnknownAs 
unknownAs) {
     if (hasCustomNullabilityRules(a.getKind())) {
       return simplifiedResult;
     }
-    if (!isSafe) {
-      return simplifiedResult;
-    }
     switch (Strong.policy(a)) {
     case NOT_NULL:
+      // Drops the subtree; require full-tree safety so we don't hide runtime 
errors
+      if (!isSafe) {
+        return simplifiedResult;
+      }
       return rexBuilder.makeLiteral(true);
     case ANY:
       // "f" is a strong operator, so "f(operand0, operand1) IS NOT NULL"
-      // simplifies to "operand0 IS NOT NULL AND operand1 IS NOT NULL"
+      // simplifies to "operand0 IS NOT NULL AND operand1 IS NOT NULL";
+      // this branch PRESERVES the operand subtrees, so it only
+      // needs SHALLOW safety of the outer operator
+      if (!SafeRexVisitor.INSTANCE.isShallowSafe(a)) {
+        return simplifiedResult;
+      }
       final List<RexNode> operands = new ArrayList<>();
       for (RexNode operand : ((RexCall) a).getOperands()) {
         final RexNode simplified = simplifyIsNotNull(operand);
@@ -1223,6 +1229,9 @@ private RexNode simplifyIs(RexCall call, RexUnknownAs 
unknownAs) {
       }
       return RexUtil.composeConjunction(rexBuilder, operands);
     case CUSTOM:
+      if (!isSafe) {
+        return simplifiedResult;
+      }
       switch (a.getKind()) {
       case LITERAL:
         return rexBuilder.makeLiteral(!((RexLiteral) a).isNull());
@@ -1261,15 +1270,18 @@ private RexNode simplifyIs(RexCall call, RexUnknownAs 
unknownAs) {
     if (hasCustomNullabilityRules(a.getKind())) {
       return simplifiedResult;
     }
-    if (!isSafe) {
-      return simplifiedResult;
-    }
     switch (Strong.policy(a)) {
     case NOT_NULL:
+      // Drops the subtree; require full-tree safety so we don't hide runtime 
errors
+      if (!isSafe) {
+        return simplifiedResult;
+      }
       return rexBuilder.makeLiteral(false);
     case ANY:
-      // "f" is a strong operator, so "f(operand0, operand1) IS NULL" 
simplifies
-      // to "operand0 IS NULL OR operand1 IS NULL"
+      // See symmetric comment in simplifyIsNotNull
+      if (!SafeRexVisitor.INSTANCE.isShallowSafe(a)) {
+        return simplifiedResult;
+      }
       final List<RexNode> operands = new ArrayList<>();
       for (RexNode operand : ((RexCall) a).getOperands()) {
         final RexNode simplified = simplifyIsNull(operand);
@@ -1595,23 +1607,34 @@ enum SafeRexVisitor implements RexVisitor<Boolean> {
     }
 
     @Override public Boolean visitCall(RexCall call) {
+      return isSafe(call, true);
+    }
+
+    private boolean isSafe(RexCall call, boolean deep) {
       SqlKind sqlKind = call.getKind();
       SqlOperator sqlOperator = call.getOperator();
 
       if (SqlKind.CHECKED_ARITHMETIC.contains(sqlKind)) {
         // Checked arithmetic throws on overflow, so it is only safe when the
         // arithmetic is never performed, i.e. when an operand is NULL.
-        return RexVisitorImpl.visitArrayAnd(this, call.operands)
-            && call.operands.stream().anyMatch(o -> RexUtil.isNullLiteral(o, 
true));
+        if (deep) {
+          boolean areOperandsSafe = RexVisitorImpl.visitArrayAnd(this, 
call.operands);
+          if (!areOperandsSafe) {
+            return false;
+          }
+        }
+        return call.operands.stream().anyMatch(o -> RexUtil.isNullLiteral(o, 
true));
       }
 
       switch (sqlKind) {
       case DIVIDE:
       case MOD:
         List<RexNode> operands = call.getOperands();
-        boolean areOperandsSafe = RexVisitorImpl.visitArrayAnd(this, 
call.operands);
-        if (!areOperandsSafe) {
-          return false;
+        if (deep) {
+          boolean areOperandsSafe = RexVisitorImpl.visitArrayAnd(this, 
call.operands);
+          if (!areOperandsSafe) {
+            return false;
+          }
         }
         boolean hasNullOperand = RexUtil.isNullLiteral(operands.get(0), true)
             || RexUtil.isNullLiteral(operands.get(1), true);
@@ -1621,7 +1644,7 @@ enum SafeRexVisitor implements RexVisitor<Boolean> {
         if (operands.get(1) instanceof RexLiteral) {
           return !checkLiteralValue(operands.get(1), BigDecimal.ZERO);
         }
-        // the safety of division could not be deduced, so assume it is unsafe
+        // the safety of MOD / DIVIDE could not be deduced, so assume it is 
unsafe
         return false;
       default:
         break;
@@ -1631,12 +1654,31 @@ enum SafeRexVisitor implements RexVisitor<Boolean> {
           || RexUtil.isLosslessCast(call)
           || safeOps.contains(sqlKind)
           || safeOperators.contains(sqlOperator)) {
-        return RexVisitorImpl.visitArrayAnd(this, call.operands);
+        return !deep || RexVisitorImpl.visitArrayAnd(this, call.operands);
       }
 
       return false;
     }
 
+    /**
+     * Shallow variant of the visitor: reports whether the OUTER node's
+     * operator can be evaluated on non-null operands without throwing at
+     * runtime. Unlike {@link #visitCall(RexCall)}, it does not recurse into
+     * the operands. Callers that only need to know whether the outer
+     * operator itself is safe (e.g. RexSimplify's {@code Strong.ANY}
+     * distribution branches, which preserve subtree evaluation) can use
+     * this in place of the full-tree {@link RexSimplify#isSafeExpression}.
+     *
+     * <p>Non-{@link RexCall} nodes are always shallow-safe (they cannot
+     * throw at their own level).
+     */
+    boolean isShallowSafe(RexNode node) {
+      if (!(node instanceof RexCall)) {
+        return true;
+      }
+      return isSafe((RexCall) node, false);
+    }
+
     @Override public Boolean visitOver(RexOver over) {
       return false;
     }
diff --git 
a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java 
b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
index 4b873fce5c..53b0f54268 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
@@ -19,14 +19,17 @@
 import org.apache.calcite.DataContext;
 import org.apache.calcite.DataContexts;
 import org.apache.calcite.adapter.java.JavaTypeFactory;
+import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
 import org.apache.calcite.plan.RelOptPredicateList;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.fun.SqlInternalOperators;
 import org.apache.calcite.sql.fun.SqlLibraryOperators;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
 
 import com.google.common.collect.ImmutableList;
@@ -65,6 +68,7 @@ public abstract class RexProgramBuilderBase {
   protected RexLiteral nullReal;
   protected RexLiteral nullDouble;
   protected RexLiteral nullVarbinary;
+  protected RexLiteral nullDate;
 
   private RelDataType nullableBool;
   private RelDataType nonNullableBool;
@@ -90,6 +94,9 @@ public abstract class RexProgramBuilderBase {
   private RelDataType nullableVarbinary;
   private RelDataType nonNullableVarbinary;
 
+  private RelDataType nullableDate;
+  private RelDataType nonNullableDate;
+
   // Note: JUnit 4 creates new instance for each test method,
   // so we initialize these structures on demand
   // It maps non-nullable type to struct of (10 nullable, 10 non-nullable) 
fields
@@ -142,6 +149,10 @@ public abstract class RexProgramBuilderBase {
     nonNullableVarbinary = typeFactory.createSqlType(SqlTypeName.VARBINARY);
     nullableVarbinary = 
typeFactory.createTypeWithNullability(nonNullableVarbinary, true);
     nullVarbinary = rexBuilder.makeNullLiteral(nullableVarbinary);
+
+    nonNullableDate = typeFactory.createSqlType(SqlTypeName.DATE);
+    nullableDate = typeFactory.createTypeWithNullability(nonNullableDate, 
true);
+    nullDate = rexBuilder.makeNullLiteral(nullableDate);
   }
 
   private RexDynamicParam getDynamicParam(RelDataType type, String 
fieldNamePrefix) {
@@ -324,6 +335,14 @@ protected RexNode mul(RexNode n1, RexNode n2) {
     return rexBuilder.makeCall(SqlStdOperatorTable.MULTIPLY, n1, n2);
   }
 
+  protected RexNode checkedPlus(RexNode n1, RexNode n2) {
+    return rexBuilder.makeCall(SqlStdOperatorTable.CHECKED_PLUS, n1, n2);
+  }
+
+  protected RexNode checkedMul(RexNode n1, RexNode n2) {
+    return rexBuilder.makeCall(SqlStdOperatorTable.CHECKED_MULTIPLY, n1, n2);
+  }
+
   protected RexNode coalesce(RexNode... nodes) {
     return rexBuilder.makeCall(SqlStdOperatorTable.COALESCE, nodes);
   }
@@ -484,6 +503,14 @@ protected RelDataType tVarbinary(boolean nullable) {
     return nullable ? nullableVarbinary : nonNullableVarbinary;
   }
 
+  protected RelDataType tDate() {
+    return nonNullableDate;
+  }
+
+  protected RelDataType tDate(boolean nullable) {
+    return nullable ? nullableDate : nonNullableDate;
+  }
+
 
   protected RelDataType tArray(RelDataType elemType) {
     return typeFactory.createArrayType(elemType, -1);
@@ -549,6 +576,13 @@ protected RexLiteral literalVarchar(String value) {
   protected RexLiteral literal(double value) {
     return rexBuilder.makeApproxLiteral(value, nonNullableDouble);
   }
+
+  protected RexLiteral interval(int value, TimeUnit timeUnit) {
+    return rexBuilder.makeIntervalLiteral(
+        BigDecimal.valueOf(value),
+        new SqlIntervalQualifier(timeUnit, null, SqlParserPos.ZERO));
+  }
+
   // Variables
 
   /**
@@ -792,6 +826,50 @@ protected RexNode vDecimalNotNull(int arg) {
     return vParamNotNull("decimal", arg, nonNullableDecimal);
   }
 
+  /**
+   * Creates {@code nullable date variable} with index of 0.
+   * If you need several distinct variables, use {@link #vDate(int)}.
+   * The resulting node would look like {@code ?0.date0}
+   *
+   * @return nullable date with index of 0
+   */
+  protected RexNode vDate() {
+    return vDate(0);
+  }
+
+  /**
+   * Creates {@code nullable date variable} with index of {@code arg} 
(0-based).
+   * The resulting node would look like {@code ?0.date3} if {@code arg} is 
{@code 3}.
+   *
+   * @param arg argument index (0-based)
+   * @return nullable date variable with given index (0-based)
+   */
+  protected RexNode vDate(int arg) {
+    return vParam("date", arg, nullableDate);
+  }
+
+  /**
+   * Creates {@code non-nullable date variable} with index of 0.
+   * If you need several distinct variables, use {@link #vDateNotNull(int)}.
+   * The resulting node would look like {@code ?0.notNullDate0}
+   *
+   * @return non-nullable date variable with index of 0
+   */
+  protected RexNode vDateNotNull() {
+    return vDateNotNull(0);
+  }
+
+  /**
+   * Creates {@code non-nullable date variable} with index of {@code arg} 
(0-based).
+   * The resulting node would look like {@code ?0.notNullDate3} if {@code arg} 
is {@code 3}.
+   *
+   * @param arg argument index (0-based)
+   * @return non-nullable date variable with given index (0-based)
+   */
+  protected RexNode vDateNotNull(int arg) {
+    return vParamNotNull("date", arg, nonNullableDate);
+  }
+
   /**
    * Creates {@code nullable variable} with given type and name of {@code arg} 
(0-based).
    * This enables cases when type is built dynamically.
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java 
b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
index ca063c4855..0dbbcd9cde 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.rex;
 
 import org.apache.calcite.avatica.util.ByteString;
+import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.plan.RelOptPredicateList;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.Strong;
@@ -3024,6 +3025,151 @@ trueLiteral, literal(1),
     checkSimplifyUnchanged(div(cast(vVarchar(), tInt(false)), nullInt));
   }
 
+  /**
+   * Test cases for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7722";>[CALCITE-7722]
+   * RexSimplify IS [NOT] NULL on a safe operator with Strong policy ANY and 
unsafe operands
+   * can be further simplified</a>.
+   */
+  @Test void testSimplifyIsNotNullDistributesAcrossStrongOpWithLossyCast() {
+    // "(CAST(?0.varchar0):INTEGER + 1) IS NOT NULL" ==> "IS NOT 
NULL(CAST(?0.varchar0):INTEGER)"
+    // The outer PLUS is strong AND shallow-safe; distribution keeps the
+    // non-lossless CAST inside the rewrapped IS NOT NULL
+    checkSimplify(
+        isNotNull(plus(cast(vVarchar(), tInt(true)), literal(1))),
+        "IS NOT NULL(CAST(?0.varchar0):INTEGER)");
+
+    // Symmetric IS NULL peel:
+    // "(CAST(?0.varchar0):INTEGER + 1) IS NULL" ==> "IS 
NULL(CAST(?0.varchar0):INTEGER)"
+    checkSimplify(
+        isNull(plus(cast(vVarchar(), tInt(true)), literal(1))),
+        "IS NULL(CAST(?0.varchar0):INTEGER)");
+
+    // Confirm this is consistent with same expression without CAST
+    checkSimplify(isNotNull(plus(vInt(), literal(1))), "IS NOT NULL(?0.int0)");
+    checkSimplify(isNull(plus(vInt(), literal(1))), "IS NULL(?0.int0)");
+
+    // MULTIPLY is also strong + shallow-safe
+    checkSimplify(
+        isNotNull(mul(cast(vVarchar(), tInt(true)), literal(2))),
+        "IS NOT NULL(CAST(?0.varchar0):INTEGER)");
+    checkSimplify(
+        isNull(mul(cast(vVarchar(), tInt(true)), literal(2))),
+        "IS NULL(CAST(?0.varchar0):INTEGER)");
+    checkSimplify(isNotNull(mul(vInt(), literal(2))), "IS NOT NULL(?0.int0)");
+    checkSimplify(isNull(mul(vInt(), literal(2))), "IS NULL(?0.int0)");
+
+    // PLUS of two non-lossless CAST
+    checkSimplify(
+        isNotNull(
+            plus(
+                cast(vVarchar(0), tInt(true)),
+                cast(vVarchar(1), tInt(true)))),
+        "AND(IS NOT NULL(CAST(?0.varchar0):INTEGER), IS NOT 
NULL(CAST(?0.varchar1):INTEGER))");
+    checkSimplify(
+        isNull(
+            plus(
+                cast(vVarchar(0), tInt(true)),
+                cast(vVarchar(1), tInt(true)))),
+        "OR(IS NULL(CAST(?0.varchar0):INTEGER), IS 
NULL(CAST(?0.varchar1):INTEGER))");
+
+    // Nested PLUS:
+    // "((CAST(?0.varchar0):INTEGER + 1) + 2) IS NOT NULL"
+    //   ==> "IS NOT NULL(CAST(?0.varchar0):INTEGER)"
+    checkSimplify(
+        isNotNull(
+            plus(plus(cast(vVarchar(), tInt(true)), literal(1)), literal(2))),
+        "IS NOT NULL(CAST(?0.varchar0):INTEGER)");
+
+    // Operators with checked arithmetic: they cannot be peeled because they 
are not "safe"
+    // (they will throw at runtime in case of overflow)
+    checkSimplifyUnchanged(
+        isNotNull(checkedPlus(cast(vVarchar(), tInt(true)), literal(1))));
+    checkSimplifyUnchanged(
+        isNull(checkedPlus(cast(vVarchar(), tInt(true)), literal(1))));
+    checkSimplifyUnchanged(
+        isNotNull(checkedMul(cast(vVarchar(), tInt(true)), literal(2))));
+    checkSimplifyUnchanged(
+        isNull(checkedMul(cast(vVarchar(), tInt(true)), literal(2))));
+
+    // Arithmetic on DATE
+    checkSimplify(
+        isNotNull(sub(vDate(), cast(vVarchar(), tDate(true)))),
+        "AND(IS NOT NULL(?0.date0), IS NOT NULL(CAST(?0.varchar0):DATE))");
+
+    // The outer PLUS is shallow-safe, but the div(1, 0) is not, so no further 
simplification occurs
+    checkSimplify(
+        isNotNull(plus(div(literal(1), literal(0)), vIntNotNull())),
+        "IS NOT NULL(/(1, 0))");
+    checkSimplify(
+        isNull(plus(div(literal(1), literal(0)), vIntNotNull())),
+        "IS NULL(/(1, 0))");
+
+    // The outer PLUS / MULT is shallow-safe, but the CAST is not 
(non-lossless),
+    // so no further simplification occurs
+    checkSimplify(isNull(plus(cast(vVarchar(), tInt(false)), literal(2))),
+        "IS NULL(CAST(?0.varchar0):INTEGER NOT NULL)");
+    checkSimplify(isNull(mul(cast(vVarchar(), tInt(false)), literal(2))),
+        "IS NULL(CAST(?0.varchar0):INTEGER NOT NULL)");
+    checkSimplify(isNotNull(plus(cast(vVarchar(), tInt(false)), literal(2))),
+        "IS NOT NULL(CAST(?0.varchar0):INTEGER NOT NULL)");
+    checkSimplify(isNotNull(mul(cast(vVarchar(), tInt(false)), literal(2))),
+        "IS NOT NULL(CAST(?0.varchar0):INTEGER NOT NULL)");
+
+    // The outer PLUS / MULT is shallow-safe, and the CAST is safe too 
(lossless CAST),
+    // so fully simplified
+    checkSimplify(isNull(plus(cast(vSmallInt(), tInt(false)), literal(2))),
+        "false");
+    checkSimplify(isNull(mul(cast(vSmallInt(), tInt(false)), literal(2))),
+        "false");
+    checkSimplify(isNotNull(plus(cast(vSmallInt(), tInt(false)), literal(2))),
+        "true");
+    checkSimplify(isNotNull(mul(cast(vSmallInt(), tInt(false)), literal(2))),
+        "true");
+
+    // IS NOT NULL(x/0) itself is not peeled, because DIVIDE with a 
literal-zero divisor is not safe
+    checkSimplifyUnchanged(isNotNull(div(vIntNotNull(), literal(0))));
+    checkSimplifyUnchanged(isNull(div(vIntNotNull(), literal(0))));
+    checkSimplifyUnchanged(isNull(div(cast(vIntNotNull(), tBigInt()), 
literal(0))));
+
+    // IS NULL(CAST(10/0 AS BIGINT)) stays as IS NULL(10/0) after the 
lossless-CAST strip;
+    // the DIVIDE is not safe, so no further distribution occurs
+    checkSimplify(isNull(cast(div(vIntNotNull(), literal(0)), tBigInt())),
+        "IS NULL(/(?0.notNullInt0, 0))");
+
+    // A bit more complex AND expression:
+    // AND(
+    // CAST(?0.varchar0):INTEGER < 100,
+    // IS NOT NULL(CAST(?0.varchar0):INTEGER + 1))
+    // ===> CAST(?0.varchar0):INTEGER < 100
+    checkSimplifyFilter(
+        and(
+            lt(cast(vVarchar(), tInt(true)), literal(100)),
+            isNotNull(plus(cast(vVarchar(), tInt(true)), literal(1)))),
+        "<(CAST(?0.varchar0):INTEGER, 100)");
+  }
+
+  @Disabled("[CALCITE-7746] Review operation safety on arithmetic on dates and 
intervals")
+  @Test void testSimplifyIsNotNullDistributesAcrossStrongOpWithIntervals() {
+    // Arithmetic on DATE and INTERVAL should be considered "unsafe" (since
+    // it can throw at runtime), so simplification should not be applied
+    checkSimplifyUnchanged(
+        isNotNull(plus(cast(vVarchar(), tDate(true)), interval(10, 
TimeUnit.DAY))));
+    checkSimplifyUnchanged(
+        isNull(plus(cast(vVarchar(), tDate(true)), interval(1, 
TimeUnit.MONTH))));
+    checkSimplifyUnchanged(
+        isNull(
+            plus(
+                plus(cast(vVarchar(), tDate(true)), interval(1, 
TimeUnit.MONTH)),
+                interval(10, TimeUnit.DAY))));
+    checkSimplifyUnchanged(
+        and(
+            isNotNull(plus(cast(vVarchar(), tDate(true)), interval(5, 
TimeUnit.MONTH))),
+            isNotNull(
+                plus(
+                    plus(cast(vVarchar(), tDate(true)), interval(1, 
TimeUnit.MONTH)),
+                    interval(10, TimeUnit.DAY)))));
+  }
+
   @Test void testPushNotIntoCase() {
     checkSimplify(
         not(

Reply via email to