Skip to content

Avoid unnecessary casting for Linq provider #2491

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

Merged
merged 21 commits into from
Sep 6, 2020
Merged

Conversation

maca88
Copy link
Contributor

@maca88 maca88 commented Aug 17, 2020

This PR contains an improvement for parameter detection that are related to nullable expressions, which was needed to avoid unnecessary casting reported in #2490.

Fixes #2490

short value = 1;
await (AssertTotalParametersAsync(
db.Users.Where(o => o.NullableShort == value && o.NullableShort != value && o.Short == value),
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.

Prior this PR there would be 3 parameters.

@@ -560,6 +577,15 @@ public async Task NullInequalityAsync()

await (ExpectAsync(session.Query<User>().Where(o => o.CreatedBy.ModifiedBy.Id != 5), Does.Contain("is null").IgnoreCase));
await (ExpectAsync(session.Query<User>().Where(o => 5 != o.CreatedBy.ModifiedBy.Id), Does.Contain("is null").IgnoreCase));

short value = 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

unnecessary cast is exists short/long with == / <= / >= operators could you add additional test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When comparing two different types short == long, a cast will always be added when the mapped sql type of short and long are different. I am thinking of restoring the previous behavior of Equals in case a user wants to compare two different types without an explicit cast as this is not possible anymore in 5.3.

Copy link
Contributor

@gokhanabatay gokhanabatay Aug 19, 2020

Choose a reason for hiding this comment

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

I think the operators and methods should work exactly the same, otherwise the software developer might make an error about which one to use and under what condition. If not careful, sql clauses are created which can cause performance problems.

The strange thing is that it casts unnecessarily in short to short or long to long comparisons.

Equals method with == operator
CompareTo method result == 0 with == operator
CompareTo method result < 0 with <= operator
CompareTo method result > 0 with >= operator

@fredericDelaporte
Copy link
Member

Does this case still qualifies as a regression? It is not really clear when reading #2490.

@maca88
Copy link
Contributor Author

maca88 commented Aug 18, 2020

The problem is that with #1996, Equals method was also modified to work the same as == operator, where previously it was directly converted without any additional logic. The new logic does not behave the same as it may add a cast where previously by using Equals was not added. This PR improves the new logic to not add a cast when both sides are casted by the compiler due to lack of an operator (short == short), but still does not match the old Equals behavior, for example:

o.Short.Equals(3)

In 5.2.x a cast is not added, where in 5.3 it is added due to a type mismatch (short and int). I think it would be better to restore the old Equals behavior in case the user does not want the new logic to add the is null check and possible a cast.

Does this case still qualifies as a regression?

Technically it is a regression but this PR does not solves it entirely, that is why I am thinking to restore the old Equals behavior, so then this PR may be added in the next version as an improvement or to the next minor as a bug fix.

@fredericDelaporte
Copy link
Member

I think we have a flaw in the casting logic: it should cast the parameter whenever possible, instead of the column. Casting a parameter has far less potential for causing performances issues than casting a column. And a parameter cast could be even pre-evaluated.
Another possibility may be to handle implicit cast detection, where it is known the SQL engine does not need the cast because it does it implicitly between the involved types. But that is very likely to be database dependent and as such quite more heavy to implement.

@gokhanabatay
Copy link
Contributor

gokhanabatay commented Aug 20, 2020

@fredericDelaporte @maca88
Take a look below test case with sql statements, as you can see there is no short parameter.
Two short column compared with casting. cast(addresses1_.ADDRESS_TYPE as int4)=cast(addresses1_.ADDRESS_TYPE2 as int4)

https://github.com/gokhanabatay/NHibernateBugTest/blob/master/NHibernateBugTest/CustomTypeSelectFailsTest.cs
AnyExistsWithNullableShort

var query = session.Query<Customer>().Where(x => x.Addresses.Any(y => y.AddressType.Value == y.AddressType2.Value));
select customer0_.GUID as guid1_1_, customer0_.NAME as name2_1_ from CUSTOMER customer0_ 
where exists (select addresses1_.GUID from ADDRESS addresses1_ where customer0_.GUID=addresses1_.CUSTOMER_GUID 
and (cast(addresses1_.ADDRESS_TYPE as int4)=cast(addresses1_.ADDRESS_TYPE2 as int4) 
or (cast(addresses1_.ADDRESS_TYPE as int4) is null) and (cast(addresses1_.ADDRESS_TYPE2 as int4) is null)))

@maca88
Copy link
Contributor Author

maca88 commented Aug 20, 2020

it should cast the parameter whenever possible, instead of the column

I do agree, I will try to implement the following rules:

  • When comparing integral numeric types, pre-evaluate the parameter o.Integer == 5L || o.Short < 4 || o.Long >= 5UL
  • When comparing integral numeric parameters with floating-point numeric columns, pre-evaluate the parameter o.Decimal == 4 || o.Double < 5L
  • When comparing floating-point numeric types, pre-evaluate the parameter o.Double == 2.3f || o.Float < 2.1d
  • When a floating-point numeric type parameter is used for integral and floating-point numeric columns, cast columns o.Double < doubleParam || o.Integer >= doubleParam
  • When a floating-point numeric type parameter is used for different floating-point numeric columns, cast the parameter o.Double < floatParam || o.Float > floatParam
  • When an integral numeric type parameter is used for different numeric columns, cast the parameter o.Double < intParam || o.Float > intParam || o.Short > intParam

Another possibility may be to handle implicit cast detection

I think that the above rules should be good enough.

Two short column compared with casting

This is already fixed with the current state of this PR, I will add additional tests for that.

@maca88
Copy link
Contributor Author

maca88 commented Aug 21, 2020

The last commit implements almost all rules as described, except for rule:

  • When a floating-point numeric type parameter is used for different floating-point numeric columns, cast the parameter

which is implemented partially. For o.Double > singleParam the parameter is casted but for o.Single > decimalParam the column is casted instead.

@maca88
Copy link
Contributor Author

maca88 commented Aug 21, 2020

Rebased.

@@ -24,7 +24,7 @@ public class FirebirdClientDriver : ReflectionBasedDriver
// Zero-width negative look-behind: the match must not be preceded by
@"(?<!" +
// a comparison,
@"[=<>]\s" +
@"[=<>]\s*" +
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted the possible regex fix for Firebird as there are other tests that would need to be modified, so tests are now skipped instead. Maybe a better fix would be to use transparentcast, similar what was done for SqLite.

@fredericDelaporte fredericDelaporte added this to the 5.3.3 milestone Aug 23, 2020
maca88 and others added 2 commits August 25, 2020 20:16
Co-authored-by: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com>
@hazzik
Copy link
Member

hazzik commented Aug 27, 2020

I did some refactoring for readability.

@maca88
Copy link
Contributor Author

maca88 commented Aug 27, 2020

Thanks!

@bahusoid
Copy link
Member

bahusoid commented Aug 28, 2020

I find it a bit confusing when 2 methods with the same name have some subtle difference in functionality - so I merged them. I hope readability is not affected )

@hazzik
Copy link
Member

hazzik commented Sep 4, 2020

@maca88 does it solve #2490? Asking for a friend Just want to know if the ticket can be closed with this PR or not.

@maca88
Copy link
Contributor Author

maca88 commented Sep 4, 2020

Yes it does, updated the description of this PR.

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

Successfully merging this pull request may close these issues.

5 participants