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])