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

xiong 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 6ee860d4c9 [CALCITE-7032] Simplify 'NULL > ALL (ARRAY[1,2,NULL])' to 
'NULL'
6ee860d4c9 is described below

commit 6ee860d4c95f025d1b34acad3564d3eee1833591
Author: Xiong Duan <[email protected]>
AuthorDate: Thu May 22 18:32:58 2025 +0800

    [CALCITE-7032] Simplify 'NULL > ALL (ARRAY[1,2,NULL])' to 'NULL'
---
 .../main/java/org/apache/calcite/plan/Strong.java  | 15 ++++
 .../java/org/apache/calcite/rex/RexAnalyzer.java   |  1 +
 .../java/org/apache/calcite/rex/RexSimplify.java   | 38 ++++++++--
 .../java/org/apache/calcite/sql/SqlOperator.java   | 21 ++++++
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java |  8 +--
 .../org/apache/calcite/rex/RexProgramTest.java     | 83 +++++++++++++++++++++-
 core/src/test/resources/sql/sub-query.iq           | 29 +++++++-
 7 files changed, 180 insertions(+), 15 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/plan/Strong.java 
b/core/src/main/java/org/apache/calcite/plan/Strong.java
index fa8fc6db61..b92c98f560 100644
--- a/core/src/main/java/org/apache/calcite/plan/Strong.java
+++ b/core/src/main/java/org/apache/calcite/plan/Strong.java
@@ -21,11 +21,13 @@
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexSubQuery;
 import org.apache.calcite.rex.RexUnknownAs;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexVisitorImpl;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.fun.SqlQuantifyOperator;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Sarg;
 
@@ -250,6 +252,19 @@ public boolean isNull(RexNode node) {
         return sarg.nullAs == RexUnknownAs.UNKNOWN;
       }
       return false;
+    case SOME:
+    case ALL:
+      final RexCall rexCall = (RexCall) node;
+      // For example:
+      // select NULL > all (select comm from emp where 1 = 0) from emp
+      // return FALSE when the sub-query returns 0 row
+      if (rexCall instanceof RexSubQuery) {
+        return false;
+      }
+      if (rexCall.getOperator() instanceof SqlQuantifyOperator) {
+        return anyNull(rexCall.getOperands());
+      }
+      return false;
     default:
       return false;
     }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java 
b/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java
index 5da2b64375..5dcad7aa9e 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java
@@ -137,6 +137,7 @@ private static class VariableCollector extends 
RexVisitorImpl<Void> {
       case M2V:
       case OTHER_FUNCTION:
       case V2M:
+      case ARRAY_VALUE_CONSTRUCTOR:
         ++unsupportedCount;
         return null;
       default:
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 ee20c5ad5d..8739274f8e 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -263,7 +263,7 @@ public RexNode simplifyUnknownAs(RexNode e, RexUnknownAs 
unknownAs) {
    * Verify adds an overhead that is only acceptable for a top-level call.
    */
   RexNode simplify(RexNode e, RexUnknownAs unknownAs) {
-    if (STRONG.isNull(e)) {
+    if (isSafeExpression(e) && STRONG.isNull(e)) {
       // Only boolean NULL (aka UNKNOWN) can be converted to FALSE. Even in
       // unknownAs=FALSE mode, we must not convert a NULL integer (say) to 
FALSE
       if (e.getType().getSqlTypeName() == SqlTypeName.BOOLEAN) {
@@ -1330,11 +1330,17 @@ enum SafeRexVisitor implements RexVisitor<Boolean> {
 
     @SuppressWarnings("ImmutableEnumChecker")
     private final Set<SqlKind> safeOps;
+    @SuppressWarnings("ImmutableEnumChecker")
+    private final ImmutableSet<SqlOperator> safeOperators;
 
     SafeRexVisitor() {
-      Set<SqlKind> safeOps = EnumSet.noneOf(SqlKind.class);
+      ImmutableSet.Builder<SqlOperator> builder = ImmutableSet.builder();
+      builder.addAll(SqlStdOperatorTable.QUANTIFY_OPERATORS);
+      safeOperators = builder.build();
 
+      Set<SqlKind> safeOps = EnumSet.noneOf(SqlKind.class);
       safeOps.addAll(SqlKind.COMPARISON);
+      safeOps.add(SqlKind.ARRAY_VALUE_CONSTRUCTOR);
       safeOps.add(SqlKind.PLUS_PREFIX);
       safeOps.add(SqlKind.MINUS_PREFIX);
       safeOps.add(SqlKind.CHECKED_MINUS_PREFIX);
@@ -1385,10 +1391,34 @@ enum SafeRexVisitor implements RexVisitor<Boolean> {
     }
 
     @Override public Boolean visitCall(RexCall call) {
-      if (!safeOps.contains(call.getKind())) {
+      SqlKind sqlKind = call.getKind();
+      SqlOperator sqlOperator = call.getOperator();
+
+      switch (sqlKind) {
+      case DIVIDE:
+      case MOD:
+        List<RexNode> operands = call.getOperands();
+        boolean isSafe = RexVisitorImpl.visitArrayAnd(this, 
ImmutableList.of(operands.get(0)));
+        if (!isSafe) {
+          return false;
+        }
+        if (operands.get(1) instanceof RexLiteral) {
+          RexLiteral literal = (RexLiteral) operands.get(1);
+          return RexUtil.isNullLiteral(literal, true);
+        }
         return false;
+      default:
+        break;
+      }
+
+      if (sqlOperator.isSafeOperator()
+          || RexUtil.isLosslessCast(call)
+          || safeOps.contains(sqlKind)
+          || safeOperators.contains(sqlOperator)) {
+        return RexVisitorImpl.visitArrayAnd(this, call.operands);
       }
-      return RexVisitorImpl.visitArrayAnd(this, call.operands);
+
+      return false;
     }
 
     @Override public Boolean visitOver(RexOver over) {
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlOperator.java 
b/core/src/main/java/org/apache/calcite/sql/SqlOperator.java
index 318318bf42..8d0755a21f 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlOperator.java
@@ -1036,6 +1036,27 @@ public <R> void acceptCall(
     return null;
   }
 
+  /**
+   * Returns whether this is a safe operator.
+   *
+   * <p>If an operator is safe, then it never causes a run-time exception.
+   *
+   * <p>For example, the {@code a/b} is not safe. Because the
+   * {@code SqlStdOperatorTable.DIVIDE} may throw the exception when dividing 
by zero.
+   *
+   * <p>By default, returns {@code false}, which means the operator is not 
safe.
+   *
+   * <p>If an operator is safe, then some optimizations can be performed in
+   * {@code org.apache.calcite.rex.RexSimplify}.
+   *
+   * <p>For example:
+   * {@code NULL + a} can be optimized to {@code NULL}.
+   *
+   */
+  public Boolean isSafeOperator() {
+    return false;
+  }
+
   /**
    * Returns whether this operator is monotonic.
    *
diff --git 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index e431129723..1b8b9d5cdb 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -4720,12 +4720,10 @@ private SqlDialect nonOrdinalDialect() {
         + "FROM dept AS d LEFT JOIN emp AS e\n"
         + " ON CASE WHEN e.job = 'PRESIDENT' THEN true ELSE d.deptno = 10 
END\n"
         + "WHERE e.job LIKE 'PRESIDENT'";
-    final String expected = "SELECT \"DEPT\".\"DEPTNO\","
-        + " \"EMP\".\"DEPTNO\" AS \"DEPTNO0\"\n"
+    final String expected = "SELECT \"DEPT\".\"DEPTNO\", \"EMP\".\"DEPTNO\" AS 
\"DEPTNO0\"\n"
         + "FROM \"SCOTT\".\"DEPT\"\n"
-        + "LEFT JOIN \"SCOTT\".\"EMP\""
-        + " ON CASE WHEN \"EMP\".\"JOB\" = 'PRESIDENT' THEN TRUE"
-        + " ELSE CAST(\"DEPT\".\"DEPTNO\" AS INTEGER) = 10 END\n"
+        + "LEFT JOIN \"SCOTT\".\"EMP\" ON \"EMP\".\"JOB\" = 'PRESIDENT' OR "
+        + "CAST(\"DEPT\".\"DEPTNO\" AS INTEGER) = 10 AND \"EMP\".\"JOB\" = 
'PRESIDENT' IS NOT TRUE\n"
         + "WHERE \"EMP\".\"JOB\" LIKE 'PRESIDENT'";
     sql(sql)
         .schema(CalciteAssert.SchemaSpec.JDBC_SCOTT)
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 1224c63298..ea44d22e9b 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -1835,6 +1835,45 @@ private void checkExponentialCnf(int n) {
         "true");
   }
 
+  /** Unit test for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7032";>[CALCITE-7032]
+   * Simplify 'NULL > ALL (ARRAY[1,2,NULL])' to 'NULL'</a>. */
+  @Test void testSimplifyQuantifyOperatorsWithArray() {
+    RexNode operand1 = nullInt;
+    RelDataType arrayType = tArray(tInt(true));
+    RelDataType arrayType2 = tArray(tSmallInt(true));
+    RexNode operand2 =
+        rexBuilder.makeCall(arrayType, 
SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+            ImmutableList.of(literal(1), literal(2), nullInt));
+    // "NULL > SOME (ARRAY[1,2,NULL])"
+    // ==> "NULL"
+    checkSimplify3(rexBuilder.makeCall(SqlStdOperatorTable.SOME_GT, operand1, 
operand2),
+        "null:BOOLEAN", "false", "true");
+
+    // "NULL > SOME (ARRAY[CAST(10 AS SMALLINT),2,NULL])"
+    // ==> "NULL"
+    operand2 =
+        rexBuilder.makeCall(arrayType, 
SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+            ImmutableList.of(cast(literal(10), tSmallInt()), literal(2), 
nullInt));
+    checkSimplify3(rexBuilder.makeCall(SqlStdOperatorTable.SOME_GT, operand1, 
operand2),
+        "null:BOOLEAN", "false", "true");
+
+    // "NULL > SOME (ARRAY[CAST(100000 AS SMALLINT),2,NULL])"
+    // ==> "NULL > SOME (ARRAY[CAST(100000 AS SMALLINT),2,NULL])"
+    operand2 =
+        rexBuilder.makeCall(arrayType, 
SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+            ImmutableList.of(cast(literal(100000), tSmallInt()), literal(2), 
nullInt));
+    checkSimplifyUnchanged(rexBuilder.makeCall(SqlStdOperatorTable.SOME_GT, 
operand1, operand2));
+
+    // "NULL > SOME (CAST(ARRAY[100000,2,NULL]) AS SMALLINT ARRAY)"
+    // ==> "NULL > SOME (CAST(ARRAY[100000,2,NULL]) AS SMALLINT ARRAY)"
+    operand2 =
+        cast(
+            rexBuilder.makeCall(arrayType, 
SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+            ImmutableList.of(literal(100000), literal(2), nullInt)), 
arrayType2);
+    checkSimplifyUnchanged(rexBuilder.makeCall(SqlStdOperatorTable.SOME_GT, 
operand1, operand2));
+  }
+
   @Test void testSimplifyRange() {
     final RexNode aRef = input(tInt(), 0);
     // ((0 < a and a <= 10) or a >= 15) and a <> 6 and a <> 12
@@ -2625,6 +2664,38 @@ trueLiteral, literal(1),
     checkSimplify(caseNode, "<=(?0.notNullInt0, 1)");
   }
 
+  /** Unit test for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7032";>[CALCITE-7032]
+   * Simplify 'NULL > ALL (ARRAY[1,2,NULL])' to 'NULL'</a>. */
+  @Test void testSimplifyDivideSafe() {
+    // null + (a/0)/4
+    // ==>
+    // null + (a/0)/4
+    simplify = simplify.withParanoid(false);
+    RexNode divideNode0 = plus(nullInt, div(div(vIntNotNull(), literal(0)), 
literal(4)));
+    checkSimplifyUnchanged(divideNode0);
+    // null + a/4
+    // ==>
+    // null + a/4
+    RexNode divideNode1 = plus(nullInt, div(vIntNotNull(), literal(4)));
+    checkSimplifyUnchanged(divideNode1);
+    // null + a/null
+    // ==>
+    // null
+    RexNode divideNode2 = plus(nullInt, div(vIntNotNull(), nullInt));
+    checkSimplify(divideNode2, "null:INTEGER");
+    // null + null/0
+    // ==>
+    // null + null/0
+    RexNode divideNode3 = plus(nullInt, div(vIntNotNull(), literal(0)));
+    checkSimplifyUnchanged(divideNode3);
+    // null + a/b
+    // ==>
+    // null + a/b
+    RexNode divideNode4 = plus(nullInt, div(vIntNotNull(), vIntNotNull()));
+    checkSimplifyUnchanged(divideNode4);
+  }
+
   @Test void testPushNotIntoCase() {
     checkSimplify(
         not(
@@ -4000,8 +4071,7 @@ private SqlSpecialOperatorWithPolicy(String name, SqlKind 
kind, int prec, boolea
   @Test void testSimplifyFunctionWithStrongPolicy() {
     final SqlOperator op =
         new SqlSpecialOperator("OP1", SqlKind.OTHER_FUNCTION, 0, false,
-            ReturnTypes.BOOLEAN, null, null) {
-        };
+            ReturnTypes.BOOLEAN, null, null);
     // Operator with no Strong.Policy defined: no simplification can be made
     checkSimplifyUnchanged(rexBuilder.makeCall(op, vInt()));
     checkSimplifyUnchanged(rexBuilder.makeCall(op, vIntNotNull()));
@@ -4011,7 +4081,7 @@ private SqlSpecialOperatorWithPolicy(String name, SqlKind 
kind, int prec, boolea
         new SqlSpecialOperatorWithPolicy("OP2", SqlKind.OTHER_FUNCTION, 0,
             false, ReturnTypes.BOOLEAN, null, null, Strong.Policy.AS_IS) {
         };
-    // Operator with Strong.Policy.AS_IS: no simplification can be made
+    // Operator with Strong.Policy.AS_IS but not safe: no simplification can 
be made
     checkSimplifyUnchanged(rexBuilder.makeCall(opPolicyAsIs, vInt()));
     checkSimplifyUnchanged(rexBuilder.makeCall(opPolicyAsIs, vIntNotNull()));
     checkSimplifyUnchanged(rexBuilder.makeCall(opPolicyAsIs, nullInt));
@@ -4019,12 +4089,19 @@ private SqlSpecialOperatorWithPolicy(String name, 
SqlKind kind, int prec, boolea
     final SqlOperator opPolicyAny =
         new SqlSpecialOperatorWithPolicy("OP3", SqlKind.OTHER_FUNCTION, 0,
             false, ReturnTypes.BOOLEAN, null, null, Strong.Policy.ANY) {
+          @Override public Boolean isSafeOperator() {
+            return true;
+          }
         };
     // Operator with Strong.Policy.ANY: simplification possible with null 
parameter
     checkSimplifyUnchanged(rexBuilder.makeCall(opPolicyAny, vInt()));
     checkSimplifyUnchanged(rexBuilder.makeCall(opPolicyAny, vIntNotNull()));
     checkSimplify3(rexBuilder.makeCall(opPolicyAny, nullInt),
         "null:BOOLEAN", "false", "true");
+    // Operator with not safe operand: no simplification can be made
+    checkSimplifyUnchanged(
+        rexBuilder.makeCall(opPolicyAny,
+            rexBuilder.makeCall(SqlStdOperatorTable.DIVIDE, vIntNotNull(), 
vIntNotNull())));
   }
 
   @Test void testSimplifyVarbinary() {
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index 0646f2c3b5..9feb458e53 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -3651,7 +3651,7 @@ select * from "scott".emp where comm in (300, 500, null);
 
 !ok
 
-EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], proj#0..7=[{exprs}], $condition=[$t10])
+EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:DECIMAL(12, 2)], expr#12=[=($t8, 
$t11)], expr#13=[OR($t10, $t12)], proj#0..7=[{exprs}], $condition=[$t13])
   EnumerableTableScan(table=[[scott, EMP]])
 !plan
 
@@ -3679,7 +3679,7 @@ select *, comm in (300, 500, null) as i from "scott".emp;
 
 !ok
 
-EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:BOOLEAN], expr#12=[OR($t10, $t11)], 
proj#0..7=[{exprs}], I=[$t12])
+EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:DECIMAL(12, 2)], expr#12=[=($t8, 
$t11)], expr#13=[OR($t10, $t12)], proj#0..7=[{exprs}], I=[$t13])
   EnumerableTableScan(table=[[scott, EMP]])
 !plan
 
@@ -3720,7 +3720,7 @@ select *, comm not in (300, 500, null) as i from 
"scott".emp;
 
 !ok
 
-EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[(-∞..300.00:DECIMAL(12, 2)), (300.00:DECIMAL(12, 
2)..500.00:DECIMAL(12, 2)), (500.00:DECIMAL(12, 2)..+∞)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:BOOLEAN], expr#12=[AND($t10, $t11)], 
proj#0..7=[{exprs}], I=[$t12])
+EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[(-∞..300.00:DECIMAL(12, 2)), (300.00:DECIMAL(12, 
2)..500.00:DECIMAL(12, 2)), (500.00:DECIMAL(12, 2)..+∞)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:DECIMAL(12, 2)], expr#12=[<>($t8, 
$t11)], expr#13=[AND($t10, $t12)], proj#0..7=[{exprs}], I=[$t13])
   EnumerableTableScan(table=[[scott, EMP]])
 !plan
 
@@ -4863,6 +4863,29 @@ EnumerableCalc(expr#0=[{inputs}], expr#1=[false], 
expr#2=[CAST($t1):BOOLEAN], DE
   EnumerableValues(tuples=[[{ 10 }, { 10 }, { 20 }, { 30 }, { 30 }, { 50 }, { 
50 }, { 60 }, { null }]])
 !plan
 
+# Same as previous; but LHS is NULL
+select deptno, null > some(select deptno from dept where false) from emp;
++--------+--------+
+| DEPTNO | EXPR$1 |
++--------+--------+
+|     10 | false  |
+|     20 | false  |
+|     30 | false  |
+|     10 | false  |
+|     30 | false  |
+|     50 | false  |
+|     50 | false  |
+|     60 | false  |
+|        | false  |
++--------+--------+
+(9 rows)
+
+!ok
+
+EnumerableCalc(expr#0=[{inputs}], expr#1=[false], expr#2=[CAST($t1):BOOLEAN], 
DEPTNO=[$t0], EXPR$1=[$t2])
+  EnumerableValues(tuples=[[{ 10 }, { 10 }, { 20 }, { 30 }, { 30 }, { 50 }, { 
50 }, { 60 }, { null }]])
+!plan
+
 # Test case about ANY sub-query when sub-query return 0 row
 select * from emp where deptno > any(select deptno from dept where false);
 +-------+--------+--------+

Reply via email to