Skip to content

Commit

Permalink
[2.0.1] Query: Fix for #9892 GroupJoin to parent with no child throws…
Browse files Browse the repository at this point in the history
… invalid operation exception

Issue: When we are trying to lift GroupJoin queries without DefaultIfEmpty, we generate LEFT JOIN which means the inner can be null too. But we still have inner key selector doing property access on inner which could throw null ref. If the inner is using EntityShaper then shaper & all access becomes null safe, but in other cases it fails.
Solution: Since inner is projecting out a non-entity, we would always have TypedProjectionShaper, which does not have null safe mechanism. Hence For such queries we need to block lifting into lift join

Resolves #9892
  • Loading branch information
smitpatel committed Oct 5, 2017
1 parent 71da832 commit 3d6caba
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 0 deletions.
41 changes: 41 additions & 0 deletions src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,19 @@ var innerShapedQuery
return false;
}

if (!(AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue9892", out var enabled) && enabled))
{
if (!IsFlattenableGroupJoinDefaultIfEmpty(groupJoinClause, queryModel, index))
{
var shaperType = innerShapedQuery?.Arguments.Last().Type;
if (shaperType == null
|| !typeof(EntityShaper).IsAssignableFrom(shaperType))
{
return false;
}
}
}

var joinClause = groupJoinClause.JoinClause;

var outerQuerySource = FindPreviousQuerySource(queryModel, index);
Expand Down Expand Up @@ -1630,6 +1643,34 @@ var newShapedQueryMethod
return true;
}

private bool IsFlattenableGroupJoinDefaultIfEmpty(
[NotNull] GroupJoinClause groupJoinClause,
QueryModel queryModel,
int index)
{
var additionalFromClause
= queryModel.BodyClauses.ElementAtOrDefault(index + 1)
as AdditionalFromClause;

var subQueryModel
= (additionalFromClause?.FromExpression as SubQueryExpression)
?.QueryModel;

var referencedQuerySource
= subQueryModel?.MainFromClause.FromExpression.TryGetReferencedQuerySource();

if (referencedQuerySource != groupJoinClause
|| queryModel.CountQuerySourceReferences(groupJoinClause) != 1
|| subQueryModel.BodyClauses.Count != 0
|| subQueryModel.ResultOperators.Count != 1
|| !(subQueryModel.ResultOperators[0] is DefaultIfEmptyResultOperator))
{
return false;
}

return true;
}

private bool TryFlattenGroupJoinDefaultIfEmpty(
[NotNull] GroupJoinClause groupJoinClause,
QueryModel queryModel,
Expand Down
92 changes: 92 additions & 0 deletions test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2689,6 +2689,98 @@ public class EntityWithLocalVariableAccessInFilter9825

#endregion

#region Bug9892

[Fact]
public virtual void GroupJoin_to_parent_with_no_child_works_9892()
{
using (CreateDatabase9892())
{
using (var context = new MyContext9892(_options))
{
var results = (
from p in context.Parents
join c in (
from x in context.Children
select new
{
ParentId = x.ParentId,
OtherParent = x.OtherParent.Name
})
on p.Id equals c.ParentId into child
select new
{
ParentId = p.Id,
ParentName = p.Name,
Children = child.Select(c => c.OtherParent)
}).ToList();

Assert.Equal(3, results.Count);
Assert.Single(results.Where(t => !t.Children.Any()));
}
}
}

private SqlServerTestStore CreateDatabase9892()
=> CreateTestStore(
() => new MyContext9892(_options),
context =>
{
context.Parents.Add(new Parent9892 { Name = "Parent1" });
context.Parents.Add(new Parent9892 { Name = "Parent2" });
context.Parents.Add(new Parent9892 { Name = "Parent3" });

context.OtherParents.Add(new OtherParent9892 { Name = "OtherParent1" });
context.OtherParents.Add(new OtherParent9892 { Name = "OtherParent2" });

context.SaveChanges();

context.Children.Add(new Child9892 { ParentId = 1, OtherParentId = 1 });
context.Children.Add(new Child9892 { ParentId = 1, OtherParentId = 2 });
context.Children.Add(new Child9892 { ParentId = 2, OtherParentId = 1 });
context.Children.Add(new Child9892 { ParentId = 2, OtherParentId = 2 });

context.SaveChanges();

ClearLog();
});

public class MyContext9892 : DbContext
{
public MyContext9892(DbContextOptions options)
: base(options)
{
}

public DbSet<Parent9892> Parents { get; set; }
public DbSet<Child9892> Children { get; set; }
public DbSet<OtherParent9892> OtherParents { get; set; }
}

public class Parent9892
{
public int Id { get; set; }
public string Name { get; set; }
public List<Child9892> Children { get; set; }
}

public class OtherParent9892
{
public int Id { get; set; }
public string Name { get; set; }
}

public class Child9892
{
public int Id { get; set; }
public int ParentId { get; set; }
public Parent9892 Parent { get; set; }
public int OtherParentId { get; set; }
public OtherParent9892 OtherParent { get; set; }
}

#endregion

private DbContextOptions _options;

private SqlServerTestStore CreateTestStore<TContext>(
Expand Down

0 comments on commit 3d6caba

Please sign in to comment.