-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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: Convert unflattened GroupJoin to correlated subquery #28998
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,8 +15,8 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; | |
public class QueryableMethodNormalizingExpressionVisitor : ExpressionVisitor | ||
{ | ||
private readonly QueryCompilationContext _queryCompilationContext; | ||
|
||
private readonly SelectManyVerifyingExpressionVisitor _selectManyVerifyingExpressionVisitor = new(); | ||
private readonly GroupJoinConvertingExpressionVisitor _groupJoinConvertingExpressionVisitor = new(); | ||
|
||
/// <summary> | ||
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
|
@@ -29,6 +29,19 @@ public QueryableMethodNormalizingExpressionVisitor(QueryCompilationContext query | |
_queryCompilationContext = queryCompilationContext; | ||
} | ||
|
||
/// <summary> | ||
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
/// the same compatibility standards as public APIs. It may be changed or removed without notice in | ||
/// any release. You should only use it directly in your code with extreme caution and knowing that | ||
/// doing so can result in application failures when updating to a new Entity Framework Core release. | ||
/// </summary> | ||
public virtual Expression Normalize(Expression expression) | ||
{ | ||
var result = Visit(expression); | ||
|
||
return _groupJoinConvertingExpressionVisitor.Visit(result); | ||
} | ||
|
||
/// <summary> | ||
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
/// the same compatibility standards as public APIs. It may be changed or removed without notice in | ||
|
@@ -332,9 +345,9 @@ private Expression TryConvertEnumerableToQueryable(MethodCallExpression methodCa | |
|| innerQueryableElementType != genericType) | ||
{ | ||
while (innerArgument is UnaryExpression | ||
{ | ||
NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs | ||
} unaryExpression | ||
{ | ||
NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs | ||
} unaryExpression | ||
&& unaryExpression.Type.TryGetElementType(typeof(IEnumerable<>)) != null) | ||
{ | ||
innerArgument = unaryExpression.Operand; | ||
|
@@ -418,7 +431,7 @@ private static bool CanConvertEnumerableToQueryable(Type enumerableType, Type qu | |
|| enumerableType == typeof(IOrderedEnumerable<>) && queryableType == typeof(IOrderedQueryable<>); | ||
} | ||
|
||
private Expression TryFlattenGroupJoinSelectMany(MethodCallExpression methodCallExpression) | ||
private MethodCallExpression TryFlattenGroupJoinSelectMany(MethodCallExpression methodCallExpression) | ||
{ | ||
var genericMethod = methodCallExpression.Method.GetGenericMethodDefinition(); | ||
if (genericMethod == QueryableMethods.SelectManyWithCollectionSelector) | ||
|
@@ -590,6 +603,76 @@ private Expression TryFlattenGroupJoinSelectMany(MethodCallExpression methodCall | |
return methodCallExpression; | ||
} | ||
|
||
private sealed class GroupJoinConvertingExpressionVisitor : ExpressionVisitor | ||
{ | ||
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) | ||
{ | ||
if (methodCallExpression.Method.DeclaringType == typeof(Queryable) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider returning early it this isn't GroupJoin, to unindent the rest of the method body. |
||
&& methodCallExpression.Method.IsGenericMethod | ||
&& methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.GroupJoin) | ||
{ | ||
var genericArguments = methodCallExpression.Method.GetGenericArguments(); | ||
var outerSource = methodCallExpression.Arguments[0]; | ||
var innerSource = methodCallExpression.Arguments[1]; | ||
var outerKeySelector = methodCallExpression.Arguments[2].UnwrapLambdaFromQuote(); | ||
var innerKeySelector = methodCallExpression.Arguments[3].UnwrapLambdaFromQuote(); | ||
var resultSelector = methodCallExpression.Arguments[4].UnwrapLambdaFromQuote(); | ||
|
||
if (innerSource.Type.IsGenericType | ||
&& innerSource.Type.GetGenericTypeDefinition() != typeof(IQueryable<>)) | ||
{ | ||
// In case of collection navigation it can be of enumerable or other type. | ||
innerSource = Expression.Call( | ||
QueryableMethods.AsQueryable.MakeGenericMethod(innerSource.Type.GetSequenceType()), | ||
innerSource); | ||
} | ||
|
||
var correlationPredicate = ReplacingExpressionVisitor.Replace( | ||
outerKeySelector.Parameters[0], | ||
resultSelector.Parameters[0], | ||
Expression.AndAlso( | ||
Infrastructure.ExpressionExtensions.CreateEqualsExpression( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to check if the outer key is null - do we support that scenario? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Join doesn't match when keys are null even if outer/inner both evaluate to null. |
||
outerKeySelector.Body, | ||
Expression.Constant(null), | ||
negated: true), | ||
Infrastructure.ExpressionExtensions.CreateEqualsExpression( | ||
outerKeySelector.Body, | ||
innerKeySelector.Body))); | ||
|
||
innerSource = Expression.Call( | ||
QueryableMethods.Where.MakeGenericMethod(genericArguments[1]), | ||
innerSource, | ||
Expression.Quote( | ||
Expression.Lambda( | ||
correlationPredicate, | ||
innerKeySelector.Parameters))); | ||
|
||
var selector = ReplacingExpressionVisitor.Replace( | ||
resultSelector.Parameters[1], | ||
innerSource, | ||
resultSelector.Body); | ||
|
||
if (genericArguments[3].IsGenericType | ||
&& genericArguments[3].GetGenericTypeDefinition() == typeof(IEnumerable<>)) | ||
{ | ||
selector = Expression.Call( | ||
EnumerableMethods.AsEnumerable.MakeGenericMethod(genericArguments[3].GetSequenceType()), | ||
selector); | ||
} | ||
|
||
return Expression.Call( | ||
QueryableMethods.Select.MakeGenericMethod(genericArguments[0], genericArguments[3]), | ||
outerSource, | ||
Expression.Quote( | ||
Expression.Lambda( | ||
selector, | ||
resultSelector.Parameters[0]))); | ||
} | ||
|
||
return base.VisitMethodCall(methodCallExpression); | ||
} | ||
} | ||
|
||
private sealed class SelectManyVerifyingExpressionVisitor : ExpressionVisitor | ||
{ | ||
private readonly List<ParameterExpression> _allowedParameters = new(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -132,14 +132,13 @@ public virtual void Throws_when_join() | |
} | ||
|
||
[ConditionalFact] | ||
public virtual void Throws_when_group_join() | ||
public virtual void Does_not_throws_when_group_join() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can probably remove this with all the other positive tests no? |
||
{ | ||
using var context = CreateContext(); | ||
AssertTranslationFailed( | ||
() => (from e1 in context.Employees | ||
(from e1 in context.Employees | ||
join i in new uint[] { 1, 2, 3 } on e1.EmployeeID equals i into g | ||
select e1) | ||
.ToList()); | ||
.ToList(); | ||
} | ||
|
||
[ConditionalFact(Skip = "Issue#18923")] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid double visitation here by calling into the GroupJoin conversion logic from VisitMethod (of QueryableMethodNormalizingExpressionVisitor)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot. Flattening of GJ-SM requires visited children and visiting it before flattening will remove GJ.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, was not aware that this same visitor also does GJ flattening. We could do something fancy by havnig a pending GroupJoin bubble up and then either flatten it on SelectMany, or do the new conversion logic if not. But that may be too much.