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 5be1fead88e..deac203607d 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 01da3e67727..2bdc7a1d56c 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 + * [CALCITE-7542] + * RexCall.isAlwaysTrue()/isAlwaysFalse() incorrectly returns true + * for CAST(boolean AS non-boolean). */ + @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 * [CALCITE-2438] * RexCall#isAlwaysTrue returns incorrect result. */ diff --git a/core/src/test/resources/sql/conditions.iq b/core/src/test/resources/sql/conditions.iq index 7ce0c78c9c2..18150050b6b 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