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 3294cd1b27 [CALCITE-7542] RexCall.isAlwaysTrue()/isAlwaysFalse() 
incorrectly returns true for CAST(boolean AS non-boolean)
3294cd1b27 is described below

commit 3294cd1b27787ed5b506cee89c83719aed266948
Author: Steven Phillips <[email protected]>
AuthorDate: Mon May 25 14:52:00 2026 -0700

    [CALCITE-7542] RexCall.isAlwaysTrue()/isAlwaysFalse() incorrectly returns 
true for CAST(boolean AS non-boolean)
    
    RexCall.isAlwaysTrue() and isAlwaysFalse() group CAST with 
IS_TRUE/IS_NOT_FALSE
    (and IS_FALSE/IS_NOT_TRUE) and delegate to the operand. That is wrong when 
the
    cast changes the type: CAST(TRUE AS INTEGER) is an INTEGER expression that
    evaluates to 1, not a boolean, so it must report isAlwaysTrue() == false.
    
    Fix: add a top-level "if (getType() != BOOLEAN) return false" guard, 
mirroring
    the pattern already used by RexLiteral.isAlwaysTrue()/isAlwaysFalse(). The
    guard subsumes the CAST case and is future-proof against new switch cases
    that might return from a non-boolean kind.
    
    Tests:
    - Five unit tests in RexProgramTest covering CAST(boolean AS INTEGER),
      CAST(boolean AS BOOLEAN), and the recursion path through
      CAST(non-boolean RexCall AS BOOLEAN).
    - An end-to-end Quidem probe in conditions.iq that exercises the CAST
      recursion path with a babel WHERE clause.
    
    Co-authored-by: Sean Broeder <[email protected]>
---
 .../main/java/org/apache/calcite/rex/RexCall.java  | 10 +++++
 .../org/apache/calcite/rex/RexProgramTest.java     | 49 ++++++++++++++++++++++
 core/src/test/resources/sql/conditions.iq          | 17 ++++++++
 3 files changed, 76 insertions(+)

diff --git a/core/src/main/java/org/apache/calcite/rex/RexCall.java 
b/core/src/main/java/org/apache/calcite/rex/RexCall.java
index 5be1fead88..deac203607 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexCall.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexCall.java
@@ -219,6 +219,12 @@ private boolean digestWithType() {
   @Override public boolean isAlwaysTrue() {
     // "c IS NOT NULL" occurs when we expand EXISTS.
     // This reduction allows us to convert it to a semi-join.
+    // Only boolean-valued calls can be always-true; e.g. CAST(TRUE AS INTEGER)
+    // evaluates to 1 (INTEGER), not a boolean, even though its operand is
+    // always true.
+    if (getType().getSqlTypeName() != SqlTypeName.BOOLEAN) {
+      return false;
+    }
     switch (getKind()) {
     case IS_NOT_NULL:
       return !operands.get(0).getType().isNullable();
@@ -241,6 +247,10 @@ private boolean digestWithType() {
   }
 
   @Override public boolean isAlwaysFalse() {
+    // Only boolean-valued calls can be always-false; see isAlwaysTrue().
+    if (getType().getSqlTypeName() != SqlTypeName.BOOLEAN) {
+      return false;
+    }
     switch (getKind()) {
     case IS_NULL:
       return !operands.get(0).getType().isNullable();
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 01da3e6772..2bdc7a1d56 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -4130,6 +4130,55 @@ private void checkSarg(String message, Sarg sarg,
     checkSimplify(isTrue(like(ref, literal("%"))), "IS NOT NULL($0)");
   }
 
+  /** Unit tests for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7542";>[CALCITE-7542]
+   * RexCall.isAlwaysTrue()/isAlwaysFalse() incorrectly returns true
+   * for CAST(boolean AS non-boolean)</a>. */
+  @Test void testIsAlwaysTrueCastBooleanToInteger() {
+    // CAST(TRUE AS INTEGER) is not a boolean expression; isAlwaysTrue() must 
be false.
+    final RexNode castTrueToInt = abstractCast(trueLiteral, tInt());
+    assertThat("CAST(TRUE AS INTEGER).isAlwaysTrue()",
+        castTrueToInt.isAlwaysTrue(), is(false));
+    assertThat("CAST(TRUE AS INTEGER).isAlwaysFalse()",
+        castTrueToInt.isAlwaysFalse(), is(false));
+  }
+
+  @Test void testIsAlwaysFalseCastBooleanToInteger() {
+    // CAST(FALSE AS INTEGER) is not a boolean expression; isAlwaysFalse() 
must be false.
+    final RexNode castFalseToInt = abstractCast(falseLiteral, tInt());
+    assertThat("CAST(FALSE AS INTEGER).isAlwaysFalse()",
+        castFalseToInt.isAlwaysFalse(), is(false));
+    assertThat("CAST(FALSE AS INTEGER).isAlwaysTrue()",
+        castFalseToInt.isAlwaysTrue(), is(false));
+  }
+
+  @Test void testIsAlwaysTrueCastBooleanToBoolean() {
+    // CAST(TRUE AS BOOLEAN) preserves the always-true property.
+    final RexNode castTrueToBool = abstractCast(trueLiteral, tBool());
+    assertThat("CAST(TRUE AS BOOLEAN).isAlwaysTrue()",
+        castTrueToBool.isAlwaysTrue(), is(true));
+  }
+
+  @Test void testIsAlwaysFalseCastBooleanToBoolean() {
+    // CAST(FALSE AS BOOLEAN) preserves the always-false property.
+    final RexNode castFalseToBool = abstractCast(falseLiteral, tBool());
+    assertThat("CAST(FALSE AS BOOLEAN).isAlwaysFalse()",
+        castFalseToBool.isAlwaysFalse(), is(true));
+  }
+
+  @Test void testIsAlwaysTrueFalseCastNonBooleanCallToBoolean() {
+    // CAST(1 + 1 AS BOOLEAN): the inner operand is a non-literal, non-boolean
+    // RexCall. isAlwaysTrue()/isAlwaysFalse() must safely return false on the
+    // recursive call rather than crashing — the contract is "ask freely, get
+    // a safe false for non-boolean expressions," matching RexLiteral and the
+    // RexNode base class.
+    final RexNode castIntExprToBool = abstractCast(plus(literal(1), 
literal(1)), tBool());
+    assertThat("CAST(1 + 1 AS BOOLEAN).isAlwaysTrue()",
+        castIntExprToBool.isAlwaysTrue(), is(false));
+    assertThat("CAST(1 + 1 AS BOOLEAN).isAlwaysFalse()",
+        castIntExprToBool.isAlwaysFalse(), is(false));
+  }
+
   /** Unit tests for
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-2438";>[CALCITE-2438]
    * RexCall#isAlwaysTrue returns incorrect result</a>. */
diff --git a/core/src/test/resources/sql/conditions.iq 
b/core/src/test/resources/sql/conditions.iq
index 7ce0c78c9c..18150050b6 100644
--- a/core/src/test/resources/sql/conditions.iq
+++ b/core/src/test/resources/sql/conditions.iq
@@ -595,4 +595,21 @@ where 5 < cast(deptno as integer) OR 5 >= cast(deptno as 
integer) OR deptno IS N
 EnumerableTableScan(table=[[scott, EMP]])
 !plan
 
+# Probe: CAST(int_expr AS BOOLEAN) in a WHERE clause exercises the recursion
+# path of RexCall.isAlwaysTrue/False through the CAST case.
+!use scott-babel
+
+with t(a) as (values (0), (1), (2))
+select * from t where cast(a + 1 as boolean);
++---+
+| A |
++---+
+| 0 |
+| 1 |
+| 2 |
++---+
+(3 rows)
+
+!ok
+
 # End conditions.iq

Reply via email to