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