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

Fix comparison of nullable result of functions #13642

Closed
dmitry-lipetsk opened this issue Oct 16, 2018 · 6 comments
Closed

Fix comparison of nullable result of functions #13642

dmitry-lipetsk opened this issue Oct 16, 2018 · 6 comments
Assignees
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@dmitry-lipetsk
Copy link
Contributor

EF Core - master source

First example:

  [Table("TEST_MODIFY_ROW")]
  public sealed class TEST_RECORD
  {
   [Key]
   [Column("TEST_ID")]
   public Int64? TEST_ID { get; set; }

   [Column("COL_TIMESTAMP", TypeName="TIMESTAMP")]
   public DateTime? DATA { get; set; }
  };//class TEST_RECORD

// ....
     var recs=db.testTable.Where(r => r.DATA==r.DATA && r.TEST_ID==testID);

EFCore generates SQL:

WHERE (("r"."COL_TIMESTAMP" = "r"."COL_TIMESTAMP") OR ("r"."COL_TIMESTAMP" IS NULL AND "r"."COL_TIMESTAMP" IS NULL)) AND ("r"."TEST_ID" = :__testID_0)

It is OK,

Second example:

     var recs=db.testTable.Where(r => r.DATA.AddYears(1)==r.DATA.AddYears(1) && r.TEST_ID==testID);

Where used extension method:

public static System.Nullable<System.DateTime> AddYears
                                           (this System.Nullable<System.DateTime> obj,
                                            System.Nullable<System.Int32>         value)

EFCore generates SQL:

WHERE (DATEADD(YEAR,1,"r"."COL_TIMESTAMP") = DATEADD(YEAR,1,"r"."COL_TIMESTAMP")) AND ("r"."TEST_ID" = :__testID_0)

From my point of view - it is problem. In second case EFCore must generate SQL like SQL from first example:

WHERE ((DATEADD(YEAR,1,"r"."COL_TIMESTAMP") = DATEADD(YEAR,1,"r"."COL_TIMESTAMP")) OR (DATEADD(YEAR,1,"r"."COL_TIMESTAMP") IS NULL AND DATEADD(YEAR,1,"r"."COL_TIMESTAMP") IS NULL)) AND ("r"."TEST_ID" = :__testID_0)

@dmitry-lipetsk
Copy link
Contributor Author

dmitry-lipetsk commented Oct 16, 2018

I found the point, where the "IS NULL"statement is builded:

https://github.com/aspnet/EntityFrameworkCore/blob/d59be61006d78d507dea07a9779c3c4103821ca3/src/EFCore.Relational/Query/ExpressionVisitors/Internal/IsNullExpressionBuildingVisitor.cs#L80-L95

I added to this method:

            if(extensionExpression.Type.IsNullableType())
            {
                AddToResult(new IsNullExpression(extensionExpression));

                return extensionExpression;
            }

And now my copy of EFCore generates expected condition for second example:

WHERE ((DATEADD(YEAR,1,"r"."COL_TIMESTAMP") = DATEADD(YEAR,1,"r"."COL_TIMESTAMP")) OR (DATEADD(YEAR,1,"r"."COL_TIMESTAMP") IS NULL AND DATEADD(YEAR,1,"r"."COL_TIMESTAMP") IS NULL)) AND ("r"."TEST_ID" = :__testID_0)

Could anybody verify this change with own tests of EFCore?

@dmitry-lipetsk
Copy link
Contributor Author

Additional changes for IsNullExpressionBuildingVisitor::VisitBinary

            if (binaryExpression.Type.IsNullableType())
            {
                AddToResult(new IsNullExpression(binaryExpression));

                return binaryExpression;
            }//if

Need for correct translation of comparisons like

r.DATA == r.DATA.Substring(0,8)+"3"

Expected condition:

(("r"."COL_VARCHAR_10" = (SUBSTRING("r"."COL_VARCHAR_10" FROM 0 + 1 FOR 8)||_utf8 '3')) OR ("r"."COL_VARCHAR_10" IS NULL AND SUBSTRING("r"."COL_VARCHAR_10" FROM 0 + 1 FOR 8)||_utf8 '3' IS NULL))

@maumar maumar self-assigned this Oct 16, 2018
@maumar
Copy link
Contributor

maumar commented Oct 17, 2018

The way I was thinking about this is that function is null if any of its arguments is null (doesn't apply to all functions like COALESCE but it does to most). So we don't need to test the entire function for NULL, just its arguments:

So it would look something like this:

WHERE ((DATEADD(YEAR,1,"r"."COL_TIMESTAMP") = DATEADD(YEAR,1,"r"."COL_TIMESTAMP")) OR ("r"."COL_TIMESTAMP" IS NULL AND "r"."COL_TIMESTAMP" IS NULL)) AND ("r"."TEST_ID" = :__testID_0)

further optimized to:

WHERE ((DATEADD(YEAR,1,"r"."COL_TIMESTAMP") = DATEADD(YEAR,1,"r"."COL_TIMESTAMP")) OR "r"."COL_TIMESTAMP" IS NULL ) AND ("r"."TEST_ID" = :__testID_0)

This would also work for functions that return non-nullable values, e.g. CHARINDEX('foo', myNullColumn) - sql will return null if myNullColumn is actually NULL, but the function itself returns int/bigint so we can't rely on return type alone here.

@dmitry-lipetsk
Copy link
Contributor Author

dmitry-lipetsk commented Oct 17, 2018

Hello @maumar,

I think, in common case (at first stage) need generate expression:

WHERE ((DATEADD(YEAR,1,"r"."COL_TIMESTAMP") = DATEADD(YEAR,1,"r"."COL_TIMESTAMP")) OR (DATEADD(YEAR,1,"r"."COL_TIMESTAMP") IS NULL AND DATEADD(YEAR,1,"r"."COL_TIMESTAMP") IS NULL)) AND ("r"."TEST_ID" = :__testID_0)

Optimization of this expression - it is optional / additional / second stage.

Personally I not want to mix this stages - it is way for bad designe.


This issue is critical for me, I hope you will find the decision.

For example, you may offer the way for replacing of IsNullExpressionBuildingVisitor at data provider level.

@dmitry-lipetsk
Copy link
Contributor Author

dmitry-lipetsk commented Oct 17, 2018

I not sure, but seem optimization of "DATEADD(YEAR,1,"r"."COL_TIMESTAMP") IS NULL))" may be implemented at PredicateReductionExpressionOptimizer level.

dmitry-lipetsk added a commit to dmitry-lipetsk/EntityFrameworkCore that referenced this issue Oct 18, 2018
…blem, which described in dotnet#13642.

Optimization of generated code is linked with dotnet#13654 and will be second task :)
@ajcvickers ajcvickers added this to the 3.0.0 milestone Oct 22, 2018
maumar added a commit that referenced this issue Oct 24, 2018
Problem was that we were not applying clr null semantics when comparing result of functions, i.e. if the function results in null and we compared it to another null value, we would treat them as non-equal. According to clr null semantics they should be treated as the same.

Fix is to expand IsNullExpression builder to handle functions with nullable arguments.

f(a, b) == null <=> a == null || b == null, unless function is COALESCE (in that case both arguments have to be null), so we can just test arguments not the entire function expression.
Also added small optimization in PredicateReductionExpressionOptimizer that removes redundant IS NULL terms.
maumar added a commit that referenced this issue Oct 24, 2018
Problem was that we were not applying clr null semantics when comparing result of functions, i.e. if the function results in null and we compared it to another null value, we would treat them as non-equal. According to clr null semantics they should be treated as the same.

Fix is to expand IsNullExpression builder to handle functions with nullable arguments.

f(a, b) == null <=> a == null || b == null, unless function is COALESCE (in that case both arguments have to be null), so we can just test arguments not the entire function expression.
Also added small optimization in PredicateReductionExpressionOptimizer that removes redundant IS NULL terms.
maumar added a commit that referenced this issue Oct 24, 2018
Problem was that we were not applying clr null semantics when comparing result of functions, i.e. if the function results in null and we compared it to another null value, we would treat them as non-equal. According to clr null semantics they should be treated as the same.

Fix is to expand IsNullExpression builder to handle functions with nullable arguments.

f(a, b) == null <=> a == null || b == null, unless function is COALESCE (in that case both arguments have to be null), so we can just test arguments not the entire function expression.
Also added small optimization in PredicateReductionExpressionOptimizer that removes redundant IS NULL terms.
maumar added a commit that referenced this issue Oct 24, 2018
Problem was that we were not applying clr null semantics when comparing result of functions, i.e. if the function results in null and we compared it to another null value, we would treat them as non-equal. According to clr null semantics they should be treated as the same.

Fix is to expand IsNullExpression builder to handle functions with nullable arguments.

f(a, b) == null <=> a == null || b == null, unless function is COALESCE (in that case both arguments have to be null), so we can just test arguments not the entire function expression.
Also added small optimization in PredicateReductionExpressionOptimizer that removes redundant IS NULL terms.
@maumar
Copy link
Contributor

maumar commented Oct 24, 2018

fixed in fd842ed

@maumar maumar closed this as completed Oct 24, 2018
@maumar maumar added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Oct 24, 2018
dmitry-lipetsk added a commit to dmitry-lipetsk/EntityFrameworkCore that referenced this issue Oct 31, 2018
@ajcvickers ajcvickers modified the milestones: 3.0.0, 3.0.0-preview2 Feb 6, 2019
@ajcvickers ajcvickers changed the title Comparison of nullable result of functions Fix comparison of nullable result of functions Mar 1, 2019
@ajcvickers ajcvickers modified the milestones: 3.0.0-preview2, 3.0.0 Nov 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

No branches or pull requests

3 participants