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

Query: chaining multiple collection navigations followed by FirstOrDefault returns incorrect results #15798

Closed
maumar opened this issue May 24, 2019 · 9 comments · Fixed by #17805
Assignees
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. type-bug
Milestone

Comments

@maumar
Copy link
Contributor

maumar commented May 24, 2019

query:

ctx.Customers
  .Select(
    c => c.Orders
      .OrderBy(o => o.Id)
      .FirstOrDefault()
      .OrderDetails
      .OrderBy(od => od.Id)
      .FirstOrDefault()
      .Name)

we translate this to:

from c in ctx.Customers
select (
  from od in ctx.OrderDetails
  where od.OrderId == (
    from o in ctx.Orders
    where o.CustomerId == c.Id
    orderby o.Id
    select o).FirstOrDefault().Id
  orderby od.Id
  select od.Name).FirstOrDefault()

Now, because we apply C# semantics to the comparison, when od.OrderId is NULL and the orders subquery is null, we match this to true and return the associated order detail. This is not expected - we should filter out elements where null == null (i.e. simulate database null semantics for this specific case).

@ajcvickers
Copy link
Member

@divega to have a bit of a think.

@divega
Copy link
Contributor

divega commented Jun 25, 2019

@maumar I found a bit interesting that the SQL translation produced by EF Core 2.2 for the second query does not include terms to compensate for three value logic and produces the correct result:

SELECT (
    SELECT TOP(1) [od].[Name]
    FROM [OrderDetails] AS [od]
    WHERE [od].[OrderId] = (
        SELECT TOP(1) [o].[Id]
        FROM [Orders] AS [o]
        WHERE [o].[CustomerId] = [c].[Id]
        ORDER BY [o].[Id]
    )
    ORDER BY [od].[Id]
)
FROM [Customers] AS [c]

While the 3.0 translation is:

SELECT (
    SELECT TOP(1) [o].[Name]
    FROM [OrderDetails] AS [o]
    WHERE (([o].[OrderId] = (
        SELECT TOP(1) [o0].[Id]
        FROM [Orders] AS [o0]
        WHERE [o0].[CustomerId] = [c].[Id]
        ORDER BY [o0].[Id])) AND ([o].[OrderId] IS NOT NULL AND (
        SELECT TOP(1) [o0].[Id]
        FROM [Orders] AS [o0]
        WHERE [o0].[CustomerId] = [c].[Id]
        ORDER BY [o0].[Id]) IS NOT NULL)) OR ([o].[OrderId] IS NULL AND (
        SELECT TOP(1) [o0].[Id]
        FROM [Orders] AS [o0]
        WHERE [o0].[CustomerId] = [c].[Id]
        ORDER BY [o0].[Id]) IS NULL)
    ORDER BY [o].[Id])
FROM [Customers] AS [c]

Although this might be just a fortunate coincidence (and the reason for it might simply be that we didn't fully implement null semantics ), I agree with you that in this case the null semantics expansion doesn't apply.

At first glance, it seems that this is true in general when we compare FK to PK properties. In other words, when we traverse relationships (e.g. also for any joins). I cannot think of a better criteria we can use to decide when this behavior applies.

Incidentally, even in LINQ to Objects, Join() does not yield rows on which both sides of the equals are null.

This also made me wonder what the translation should be if Order had a composite key, but currently (with preview6) the query is broken (exception at the end).

It seems to me that it should be possible to produce a translation that would work with composite keys using joins and window functions (again!) to return the first rows of orders and order details. Is there something simpler?

Here is the exception message you get in preview 6 with a composite key on Order:

System.InvalidOperationException
  HResult=0x80131509
  Message=Operation is not valid due to the current state of the object.
  Source=Microsoft.EntityFrameworkCore.Relational
  StackTrace:
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateOrderBy(ShapedQueryExpression source, LambdaExpression keySelector, Boolean ascending)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.Pipeline.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.Pipeline.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSubquery(Expression expression)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at Microsoft.EntityFrameworkCore.SqlServer.Query.Pipeline.SqlServerSqlTranslatingExpressionVisitor.VisitBinary(BinaryExpression binaryExpression)
   at System.Linq.Expressions.BinaryExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.Pipeline.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.Pipeline.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.Pipeline.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSubquery(Expression expression)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalSqlTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalSqlTranslatingExpressionVisitor.Translate(Expression expression)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalProjectionBindingExpressionVisitor.Visit(Expression expression)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalProjectionBindingExpressionVisitor.Translate(SelectExpression selectExpression, Expression expression)
   at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.Pipeline.QueryCompilationContext2.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetEnumerator()
   at System.Collections.Generic.List`1.AddEnumerable(IEnumerable`1 enumerable)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Repro15798.Program.Main(String[] args) in C:\Users\divega\source\repos\Repro15798\Program.cs:line 32

@maumar
Copy link
Contributor Author

maumar commented Jun 25, 2019

In 2.2 we deliberately did not apply null semantics in this case. When constructing a collection navigation we created dedicated expression representing correlation predicate, and when applying null semantics we would not apply null semantics for it (since it was custom expression, it was super easy to detect the case). That was my original plan but @smitpatel was adamant against it.

Wrt composite keys, I was thinking about something like this (third example): #15260 but I'm not sure if the approach can be generalized.

@divega
Copy link
Contributor

divega commented Jun 25, 2019

When constructing a collection navigation we created dedicated expression representing correlation predicate, and when applying null semantics we would not apply null semantics for it

Ok, I believe the solution would look like this, but I think we need to think trough and come up with a rational rule for when the behavior applies. E.g. is it really only for collection navigations? What about regular joins and references? I think it is all of the above because you simply never use null values to establish a relationship.

Wrt composite keys, I was thinking about something like thi

Yep, that looks like it should work.

@smitpatel
Copy link
Member

When constructing a collection navigation we created dedicated expression representing correlation predicate, and when applying null semantics we would not apply null semantics for it

Incorrect. We did not do that at all. It is in where predicate and we did not fully expand it since we assumed null is fall so we don't need it.

The only case we did not apply null semantics in 2.x was single key join. Even for composite key we just went along and applied null semantics. (Though we may have treated it like predicate)

@maumar
Copy link
Contributor Author

maumar commented Jun 25, 2019

@smitpatel we created NullSafeEqualExpression (bad name) when expanding collection navigation and during translation we wrapped it around NullCompensatedExpression so that null semantics doesnt peek in. It does seem wrong for complex cases where scalar subquery is part of the correlation predicate and it has some stuff inside that would need null semantics. But for cases of simple navigation access it was fine.

@smitpatel
Copy link
Member

As of now in new pipeline, I don't see NullSafeEqualExpression while translating to SQL.
So I am not able to understand your comment #15798 (comment)

The thing which I argued against was to generate null check for outer key rather than inner key.

@divega
Copy link
Contributor

divega commented Jun 25, 2019

I discussed this further with @smitpatel. Here are some conclusions:

  1. There is an agreement that equality with three-value logic does the right thing when traversing relationships so null compensation isn't necessary for these equality comparisons. This applies to anything that navigation expansion produces, and also to ad-hoc relationships introduced in join operations.

  2. For the particular case of this query, @smitpatel has in mind a different solution that will add a term to make the key comparisons null-safe. This should have the advantage that it makes the query return the correct results and avoid throwing in an in-memory implementation. It also requires an additional optimization (@smitpatel to provide the issue number) to actually result into a simple equality that isn't expanded by null semantics.

  3. We already correctly don't apply null semantics to the equality for single key join, but we do apply null semantics for composite key joins. This is only based on replicating the behavior in LINQ to Objects: simple keys that are null are not a match for Join(), but for composite keys (usually anonymous type instances) a member by member comparison is performed and if same members have null values then there can be a match. I think we should avoid doing this. I have created Avoid applying null semantics expansion to joins between composite keys #16265 to track this.

@maumar
Copy link
Contributor Author

maumar commented Sep 12, 2019

verified this issue has been fixed in 3.0

@maumar maumar modified the milestones: Backlog, 3.0.0 Sep 12, 2019
@maumar maumar added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed punted-for-3.0 labels Sep 12, 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. type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants