Skip to content

[CALCITE-7542] RexCall.isAlwaysTrue()/isAlwaysFalse() incorrectly returns true for CAST(boolean AS non-boolean)#4959

Open
sbroeder wants to merge 2 commits into
apache:mainfrom
sbroeder:7542
Open

[CALCITE-7542] RexCall.isAlwaysTrue()/isAlwaysFalse() incorrectly returns true for CAST(boolean AS non-boolean)#4959
sbroeder wants to merge 2 commits into
apache:mainfrom
sbroeder:7542

Conversation

@sbroeder
Copy link
Copy Markdown
Contributor

@sbroeder sbroeder commented May 22, 2026

Summary
RexCall.isAlwaysTrue() and isAlwaysFalse() group CAST together with IS_TRUE / IS_NOT_FALSE (and IS_FALSE / IS_NOT_TRUE) and simply 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. Today it returns true.

This is an API-contract violation. RexSimplify happens not to mis-fire on this expression upstream (its Comparison helper requires a LITERAL-kind operand, which CAST is not), but any caller that consults isAlwaysTrue/False on an arbitrary RexNode and trusts the result to be boolean-valued can be misled.

Tests
core/src/test/java/org/apache/calcite/rex/RexProgramTest.java:

  • testIsAlwaysTrueCastBooleanToInteger — CAST(TRUE AS INTEGER).isAlwaysTrue() is false. Fails on unfixed code.
  • testIsAlwaysFalseCastBooleanToInteger — CAST(FALSE AS INTEGER).isAlwaysFalse() is false. Fails on unfixed code.
  • testIsAlwaysTrueCastBooleanToBoolean — CAST(TRUE AS BOOLEAN).isAlwaysTrue() is still true (no regression for same-type casts).
  • testIsAlwaysFalseCastBooleanToBoolean — CAST(FALSE AS BOOLEAN).isAlwaysFalse() is still true.

Repro (against unfixed code)

./gradlew :core:test
--tests "org.apache.calcite.rex.RexProgramTest.testIsAlwaysTrueCastBooleanToInteger"
--tests "org.apache.calcite.rex.RexProgramTest.testIsAlwaysFalseCastBooleanToInteger"

Both fail with Expected: is but: was . With the fix applied, all four tests pass.

CALCITE-7542

Fix
Return early in RexCall in both methods and only delegate to the operand when the result type is BOOLEAN.

  • core/src/main/java/org/apache/calcite/rex/RexCall.java

case IS_TRUE:
case CAST:
return operands.get(0).isAlwaysTrue();
case CAST:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By adding an assertion that the type of the expression is Boolean you may catch other misuses.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great observation. I chose to use a test for BOOLEAN and return false because I am worried about assert not firing in production code. I think this is safe and covers the case where future cases might be added.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a bug to call this function with non-boolean expressions, it should be an assert or an explicit exception; by returning early you are hiding the bug.

Unfortunately there is no JavaDoc telling us what the precondtions are. I personally prefer as many assertions as possible, to find bugs early.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe it is a bug to call this function on non-boolean expressions and there could be recursion in it related to casts from
return operands.get(0).isAlwaysTrue();
Consider the expression CAST(int_expr AS BOOLEAN) where int_expr is a RexCall (eg. 1 + 1), then with the assertion it will fail, but with the early return it works correctly.

I've added a new test to demonstrate this as well testIsAlwaysTrueFalseCastNonBooleanCallToBoolean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today this seems to be called only on predicates and join conditions.
These should all be Boolean.
I will review this version.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you make changes before approval you should generally use new commits, especially if the previous one have been reviewed. Makes it easier for reviewers to see what's new.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but previously your early return was only in the CAST branch.
What happens if you move it back there and change the top one with an assertion?
Does any test fail?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My apologies and thank you for your patience. My attempt at efficiency failed miserably.
I've restored the previous logic that lived in the CAST case and all tests currently pass.

If I add a top level assert
222 + assert getType().getSqlTypeName() == SqlTypeName.BOOLEAN 223 + : "isAlwaysTrue() called on non-boolean: " + getType();
then these 3 tests will fail

  • testIsAlwaysTrueCastBooleanToInteger
  • testIsAlwaysFalseCastBooleanToInteger
  • testIsAlwaysTrueFalseCastNonBooleanCallToBoolean

Perhaps you have a different idea of where to place the assert?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because these test which you wrote do not respect the new precondition of the function. Do these tests correspond to a situation you have observed in practice? If so, can you supply an end-to-end test which illustrates this case?

If not, a solution is to simply delete these tests, since they do not exercise a "real" possible path.

…urns 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: Return early in RexCall and only delegate when
the result type is BOOLEAN.

Co-authored-by: Sean Broeder <sean@dremio.com>
Per review feedback: instead of an early-return at the top of
isAlwaysTrue/False() that checks for BOOLEAN type, do the check only
inside the CAST case where the type can diverge from the operand's.

The other switch cases (IS_*, NOT, SEARCH) all produce BOOLEAN results
by construction, so they do not need the guard.
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants