Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CALCITE-6111] Explicit cast from expression to numeric type doesn't check overflow #3522

Merged
merged 1 commit into from
Jan 19, 2024

Conversation

mihaibudiu
Copy link
Contributor

This also fixes a few more tests which were disabled in SqlOperatorTests.
I still have to handle casts to decimal values, but hopefully this fixes the behavior of casts to integer types.
This behavior also has to be documented, so a future PR will describe in more details how Calcite evaluates casts.

@mihaibudiu
Copy link
Contributor Author

The CI failure is from the Druid tests, so I guess this PR is ready for review.

@mihaibudiu
Copy link
Contributor Author

These tests are very useful. They cover many more cases than this PR handles. Here we only do casts.

".* out of range", true);
f.checkFails("SELECT cast(1e60+30 as int)",
".* out of range", true);
f.checkFails("SELECT cast(1e60+30 as bigint)",
Copy link
Contributor

Choose a reason for hiding this comment

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

what about: SELECT CAST(2147483648 as BIGINT) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These cases should be covered by the test testCastExactNumericLimits, where I re-enabled some of the tests that were disabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, this cast in particular should not fail, so I guess it's not covered by the NumericLimits test.
I have checked that this test would pass.
Are you suggesting adding tests where I use the numeric limits for a type (INT) when casting to a different type (BIGINT)?

@mihaibudiu
Copy link
Contributor Author

This is a bugfix PR, can someone please review it?

* Explicit cast from expression to numeric type doesn't check overflow</a>. */
@Test public void testOverflow() {
final SqlOperatorFixture f = fixture();
f.checkFails("SELECT cast(100+30 as tinyint)",
Copy link
Contributor

Choose a reason for hiding this comment

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

as for me - it`s hard to parse such a tests.

Suggested change
f.checkFails("SELECT cast(100+30 as tinyint)",
f.checkFails(String.format("SELECT cast(%d+30 as tinyint)", Byte.MAX_VALUE),

and the same for all near

Copy link
Contributor

Choose a reason for hiding this comment

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

what about implicit casts, is it all ok there ? or it out of scope ?
SELECT 9223372036854775807 + 1;
SELECT 2147483647 + 1;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This PR is only about casts. I plan a follow up PR about arithmetic, which has been broken for many years. There are many disabled tests because of this. That PR will handle the expressions you describe.

Regarding implicit casts: in a normal compiler, after validation the compiler should convert all implicit casts into explicit casts. This leaves many fewer cases to worry about for the subsequent stages. Unfortunately it doesn't look like Calcite works this way. I don't know enough about validation to promise I will fix this part.

However, implicit casts usually convert values from a narrow type to a wider type, so they should not be a cause of runtime failures.

But only writing lots of tests will ensure that all cases are properly handled.

@mihaibudiu
Copy link
Contributor Author

@zstan I have rewritten the tests you suggested to make the intent clearer.
Thank you for the review, please let me know if you have more suggestions.
Note that this PR does not intend to handle casts to decimal types, that will be a separate PR.
It does not handle arithmetic overflow either, that will be yet another PR.

@mihaibudiu
Copy link
Contributor Author

@zstan, I would like to merge this PR, since the fixes I plan to address depend on these being merged.
Please let me know if you have more comments. I will interpret silence as an approval if you don't mind.

@zstan
Copy link
Contributor

zstan commented Jan 14, 2024

@mihaibudiu honestly, overall looks good but as for me this issue need additional eyes here, can you found one more reviewer on dev list ?

Copy link
Member

@asolimando asolimando left a comment

Choose a reason for hiding this comment

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

LGTM, few comments but nothing major

f.checkCastFails("'" + numeric.maxOverflowNumericString + "'",
type, OUT_OF_RANGE_MESSAGE, true, castType);
type, "Number has wrong format.*", true, castType);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: maybe we can introduce a variable WRONG_FORMAT_MESSAGE in the same spirit of OUT_OF_RANGE_MESSAGE and others?

Comment on lines 13706 to 13708
f.checkFails(String.format(Locale.US, "SELECT cast(%d+30 as tinyint)", Byte.MAX_VALUE),
".* out of range", true);
Copy link
Contributor

Choose a reason for hiding this comment

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

I tried to see what happens if I locally change expectedError to something else...
Surprisingly the tests continue passing
it seems that the reason is that this method is called

@Override public void checkFails(StringAndPos sap, String expectedError,
boolean runtime) {
final String sql = "values (" + sap.addCarets() + ")";
if (runtime) {
// We need to test that the expression fails at runtime.
// Ironically, that means that it must succeed at prepare time.
SqlValidator validator = factory.createValidator();
SqlNode n = parseAndValidate(validator, sql);
assertNotNull(n);
tester.checkFails(factory, sap, expectedError, runtime);
} else {
checkQueryFails(StringAndPos.of(sql),
expectedError);
}
}

which means it checks for failure only if it is not runtime...
so it could be any expected message and any number under cast and it continues passing...

Or did I do something wrong?

Copy link
Member

Choose a reason for hiding this comment

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

@snuyanzin, I think it should fail as you say, there seems to be something wrong.

I also tried changing runtime to false just to see, in this way it always fails (with a cast with overflow, and cast without, in both cases parsing succeeds, so the check fail throws an error), I guess there is something broken in the test auxiliary methods we rely on here.

The source of the problem lies outside the present PR, but it's nonetheless a blocker for merging (if confirmed), as we can't rely on the unit tests.

Maybe @mihaibudiu has more familiarity with this part of the code, and can shed some light.

Copy link
Member

Choose a reason for hiding this comment

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

I was expecting these tests to exercise the new code added to RexToLixTranslator, but this doesn't seem to be the case (a breakpoint there isn't hit when debugging such tests).

If that's a wrong assumption, what tests are covering the new code added to RexToLixTranslator, and what part of the code is specifically tested by above tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you call the test from SqlOperatorTest it doesn't involve the evaluation.
You have to call the test from CalciteSqlOperatorTest. That class extends SqlOperatorTest but uses a test fixture which also evaluates the expressions. In the IDE you can do this easiest if you paste the following in CalciteSqlOperatorTest:

  @Test
  public void testOverflow() {
    super.testOverflow();
  }

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the refresher Mihai, I always forget about this behaviour in some of our test classes.

I have verified the tests, they correctly fail if a non-overflowing value/expression is passed, so they do their job.

Copy link
Contributor

Choose a reason for hiding this comment

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

Was able to test, thanks for clarification

Copy link
Member

@asolimando asolimando left a comment

Choose a reason for hiding this comment

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

After @snuyanzin's comment I feel we need to get to the bottom of the test issue before merging, therefore marking the PR as "request changes" until clarified

@mihaibudiu
Copy link
Contributor Author

After @snuyanzin's comment I feel we need to get to the bottom of the test issue before merging, therefore marking the PR as "request changes" until clarified

I have made the fixes you suggested. The tests should be run from the class CalciteSqlOperatorTest to exercise the evaluator.

@asolimando
Copy link
Member

After @snuyanzin's comment I feel we need to get to the bottom of the test issue before merging, therefore marking the PR as "request changes" until clarified

I have made the fixes you suggested. The tests should be run from the class CalciteSqlOperatorTest to exercise the evaluator.

Thanks Mihai, the changes and the patch LGTM, can you please address the Sonar violation regarding the repeated occurrence of the string ".* out of range"? (just define it once at the beginning of the test for better maintainability in case the error messages changes).

PS: please don't force-push while the review process is still on-going, it makes reviewers' life more difficult for not much gain, thanks!

// Casting a floating point value larger than the maximum allowed value.
// 1e60 is larger than the largest BIGINT value allowed.
f.checkFails("SELECT cast(1e60+30 as tinyint)",
".* out of range", true);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:
since there is already existing org.apache.calcite.sql.test.SqlOperatorFixture#OUT_OF_RANGE_MESSAGE would it make sense to reuse it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My last commit does exactly that. It removes one sonarcloud complaint, as pointed out by @asolimando.

The other sonarcloud complaint is about a complex method which I didn't really write, but I slightly modified, so I didn't try to reduce the complexity. This is after all just a test method.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, Sonar has false positives sometimes, it's fine to address only what's reasonable (I consider fixing the complexity of an existing test method as as nice-to-have and totally optional), but most of the time it provides good hints for keeping the code quality high.

Copy link

sonarcloud bot commented Jan 18, 2024

Quality Gate Passed Quality Gate passed

The SonarCloud Quality Gate passed, but some issues were introduced.

1 New issue
0 Security Hotspots
97.9% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarCloud

Copy link
Member

@asolimando asolimando left a comment

Choose a reason for hiding this comment

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

LGTM, thank you, Mihai!

…check overflow

Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
@mihaibudiu mihaibudiu merged commit e2c84a6 into apache:main Jan 19, 2024
15 of 16 checks passed
@mihaibudiu mihaibudiu deleted the issue6111 branch January 19, 2024 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants