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

mihaibudiu 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 1317946ea1 [CALCITE-7619] RexSimplify incorrectly simplifies 
IS_FALSE(x) when x is nullable
1317946ea1 is described below

commit 1317946ea1c318ca7cc895031ce8ca48777fcf2d
Author: Mihai Budiu <[email protected]>
AuthorDate: Tue Jun 23 11:34:41 2026 -0700

    [CALCITE-7619] RexSimplify incorrectly simplifies IS_FALSE(x) when x is 
nullable
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../java/org/apache/calcite/rex/RexSimplify.java   |  8 +++++++
 .../java/org/apache/calcite/tools/RelBuilder.java  |  6 ++++-
 .../org/apache/calcite/rex/RexProgramTest.java     | 27 ++++++++++++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.java   | 24 +++++++++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    | 19 +++++++++++++++
 .../apache/calcite/test/SqlToRelConverterTest.xml  |  4 ++--
 core/src/test/resources/sql/sub-query.iq           |  2 +-
 7 files changed, 86 insertions(+), 4 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 0ffb60454e..ae4b3501c9 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -200,6 +200,14 @@ public RexNode simplifyPreservingType(RexNode e, 
RexUnknownAs unknownAs,
         && SqlTypeUtil.equalSansNullability(rexBuilder.typeFactory, 
e2.getType(), e.getType())) {
       return e2;
     }
+    // If simplification widens nullability (NOT NULL → nullable) without 
changing
+    // the base type, using a CAST to NOT NULL is wrong: e.g.
+    // x IS FALSE is not the same as CAST(NOT(x) AS BOOLEAN NOT NULL).
+    // Return the original expression to preserve both semantics and type.
+    if (!e.getType().isNullable() && e2.getType().isNullable()
+        && SqlTypeUtil.equalSansNullability(rexBuilder.typeFactory, 
e2.getType(), e.getType())) {
+      return e;
+    }
     final RexNode e3 = rexBuilder.makeCast(e.getType(), e2, matchNullability, 
false);
     if (e3.equals(e)) {
       return e;
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java 
b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index cf9ccecdef..54d662a3a1 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -1938,9 +1938,13 @@ public RelBuilder filter(Iterable<CorrelationId> 
variablesSet,
     if (config.simplify()) {
       conjunctionPredicates = simplifier.simplifyFilterPredicates(predicates);
     } else {
+      // The config says "do not simplify", but without the following 
optimizations
+      // filter construction may fail because the predicates do not respect
+      // invariants checked by the filter constructor in Filter.isValid().
       List<RexNode> simplified = new ArrayList<>();
       for (RexNode predicate : predicates) {
-        RexNode simple = RexSimplify.simplifyComparisonWithNull(predicate, 
getRexBuilder());
+        RexNode simple = simplifier.removeNullabilityCast(predicate);
+        simple = RexSimplify.simplifyComparisonWithNull(simple, 
getRexBuilder());
         simplified.add(simple);
       }
       conjunctionPredicates =
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 ac7b5aebe6..91af635e52 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -3841,6 +3841,33 @@ private static String getString(Map<RexNode, RexNode> 
map) {
     assertThat(result2.getOperands().get(0), is(booleanInput));
   }
 
+  /** Test cases for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7619";>[CALCITE-7619]
+   * RexSimplify incorrectly simplifies IS_FALSE(x) when x is nullable</a>. */
+  @Test void testSimplifyPreservingTypeIsNotNullCast() {
+    // IS_FALSE(nullable_bool) has type BOOLEAN NOT NULL.
+    final RexNode isFalseExpr = isFalse(vBool());
+    assertThat("IS_FALSE has NOT NULL type", 
isFalseExpr.getType().isNullable(), is(false));
+    final RexNode s0 = simplify.simplifyPreservingType(isFalseExpr, 
RexUnknownAs.FALSE, true);
+    // nullable_bool IS FALSE != CAST(NOT(nullable_bool) AS BOOL NOT NULL)
+    assertThat(s0.isA(SqlKind.CAST), is(false));
+
+    // simplify(IS_FALSE(nullable_bool), FALSE) = NOT(nullable_bool), which is 
nullable.
+    final RexNode s1 = simplify.simplify(isFalseExpr, RexUnknownAs.FALSE);
+    assertThat(s1.isA(SqlKind.NOT), is(true));
+    assertThat(s1.getType().isNullable(), is(true));
+
+    // IS_TRUE(nullable_bool) has type BOOLEAN NOT NULL.
+    final RexNode isTrueExpr = isTrue(vBool());
+    assertThat(isTrueExpr.getType().isNullable(), is(false));
+    final RexNode s2 = simplify.simplifyPreservingType(isTrueExpr, 
RexUnknownAs.FALSE, true);
+    // nullable_bool IS TRUE != CAST(nullable_bool AS BOOL NOT NULL)
+    assertThat(s2.isA(SqlKind.CAST), is(false));
+
+    // simplify(IS_TRUE(nullable_bool), FALSE) = nullable_bool, which is 
nullable.
+    final RexNode s3 = simplify.simplify(isTrueExpr, RexUnknownAs.FALSE);
+    assertThat(s3.getType().isNullable(), is(true));
+  }
+
   @Test void testSimplifyNot() {
     // "NOT(NOT(x))" => "x"
     checkSimplify(not(not(vBool())), "?0.bool0");
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java 
b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 715d9f4f7b..b10a5c065c 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -87,6 +87,7 @@
 import org.apache.calcite.rel.rules.LoptOptimizeJoinRule;
 import org.apache.calcite.rel.rules.MeasureRules;
 import org.apache.calcite.rel.rules.MultiJoin;
+import org.apache.calcite.rel.rules.MultiJoinOptimizeBushyRule;
 import org.apache.calcite.rel.rules.ProjectCorrelateTransposeRule;
 import org.apache.calcite.rel.rules.ProjectFilterTransposeRule;
 import org.apache.calcite.rel.rules.ProjectJoinTransposeRule;
@@ -2328,6 +2329,29 @@ private void 
checkSemiOrAntiJoinProjectTranspose(JoinRelType type) {
         .check();
   }
 
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7619";>[CALCITE-7619]
+   * RexSimplify incorrectly simplifies IS_FALSE(x) when x is nullable</a>. */
+  @Test void testExpressionSimplification3() {
+    final String sql = "WITH tmp(bool_col) AS (\n"
+        + "    VALUES (TRUE),\n"
+        + "           (FALSE),\n"
+        + "           (NULL)\n"
+        + ")\n"
+        + "SELECT *\n"
+        + "FROM tmp\n"
+        + "WHERE bool_col IS FALSE";
+    // Simplify actually hides the bug we are trying to solve, so we disable 
it.
+    // Without this fix the reduce rule will fail with an assertion failure
+    // becase it tries to create a filter with a cast that strips nullability.
+    RelBuilder.Config config = RelBuilder.Config.DEFAULT.withSimplify(false);
+    RelOptRule reduce =
+        
ReduceExpressionsRule.FilterReduceExpressionsRule.FilterReduceExpressionsRuleConfig.DEFAULT
+        .withRelBuilderFactory(RelBuilder.proto(config)).toRule();
+    sql(sql)
+        .withRule(reduce)
+        .checkUnchanged();
+  }
+
   @Test void testReduceAverage() {
     final String sql = "select name, max(name), avg(deptno), min(name)\n"
         + "from sales.dept group by name";
diff --git 
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml 
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 29e539a0f7..2a47eefd0d 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -5565,6 +5565,25 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], 
MGR=[$3], HIREDATE=[$4], SAL=[$
 LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], 
SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
   LogicalFilter(condition=[SEARCH($1, Sarg[(-∞..'':VARCHAR(20)), 
('':VARCHAR(20)..'3':VARCHAR(20)), ('3':VARCHAR(20)..+∞)]:VARCHAR(20))])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testExpressionSimplification3">
+    <Resource name="sql">
+      <![CDATA[WITH tmp(bool_col) AS (
+    VALUES (TRUE),
+           (FALSE),
+           (NULL)
+)
+SELECT *
+FROM tmp
+WHERE bool_col IS FALSE]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(BOOL_COL=[$0])
+  LogicalFilter(condition=[IS FALSE($0)])
+    LogicalValues(tuples=[[{ true }, { false }, { null }]])
 ]]>
     </Resource>
   </TestCase>
diff --git 
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml 
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 1d05fd1b2f..b5361cbbd3 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -98,10 +98,10 @@ from empnullables]]>
     <Resource name="plan">
       <![CDATA[
 LogicalAggregate(group=[{}], EXPR$0=[COUNT() FILTER $0])
-  LogicalProject($f0=[CAST(IN($0, {
+  LogicalProject($f0=[IS TRUE(IN($0, {
 LogicalProject(DEPTNO=[$7])
   LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
-})):BOOLEAN NOT NULL])
+}))])
     LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
 ]]>
     </Resource>
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index 4b699d14b8..2f190ed040 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -2425,7 +2425,7 @@ select sal from "scott".emp e
 
 !ok
 !if (use_old_decorr) {
-EnumerableCalc(expr#0..5=[{inputs}], expr#6=[RAND()], 
expr#7=[CAST($t6):INTEGER NOT NULL], expr#8=[2], expr#9=[MOD($t7, $t8)], 
expr#10=[3], expr#11=[=($t9, $t10)], expr#12=[OR($t11, $t3)], SAL=[$t1], 
$condition=[$t12])
+EnumerableCalc(expr#0..5=[{inputs}], expr#6=[RAND()], 
expr#7=[CAST($t6):INTEGER NOT NULL], expr#8=[2], expr#9=[MOD($t7, $t8)], 
expr#10=[3], expr#11=[=($t9, $t10)], expr#12=[IS NOT FALSE($t3)], expr#13=[IS 
NOT NULL($t3)], expr#14=[AND($t12, $t13)], expr#15=[OR($t11, $t14)], SAL=[$t1], 
$condition=[$t15])
   EnumerableMergeJoin(condition=[=($2, $4)], joinType=[left])
     EnumerableSort(sort0=[$2], dir0=[ASC])
       EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], SAL=[$t5], 
DEPTNO=[$t7])

Reply via email to