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

Navigation expansion rewrite - first draft #15383

Merged
merged 1 commit into from
Apr 30, 2019
Merged

Navigation expansion rewrite - first draft #15383

merged 1 commit into from
Apr 30, 2019

Conversation

maumar
Copy link
Contributor

@maumar maumar commented Apr 17, 2019

Known limitations:

  • GroupBy: not supported at all
  • Include tracking/fixup: include method (one that suppose to do fixup and tracking) is just a stub currently - only does fixup one way and doesn't perform tracking at all,
  • Various warnings and negative cases: we used to warn/throw particular exceptions for negative cases, those are now different and the messages themselves are not hardcoded and not polished
  • include/project collection - correlated collection feature is disabled, so projecting collections will issue N+1 queries. Same goes for include collection.
  • async + collections - no special handling of async, projecting/including collection may cause a deadlock
  • owned/weak entities: not supported, we try to treat them as any other entity at the moment
  • query filters: not taken into account currently

@maumar maumar requested a review from smitpatel April 17, 2019 00:25
@@ -11,6 +11,7 @@ Microsoft.EntityFrameworkCore.DbSet
<MinClientVersion>3.6</MinClientVersion>
<AssemblyName>Microsoft.EntityFrameworkCore</AssemblyName>
<RootNamespace>Microsoft.EntityFrameworkCore</RootNamespace>
<NoWarn>$(NoWarn);CS1591</NoWarn>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

disabled for now, too much work to put those in, and the codebase is in flux


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion
{
public class CustomRootExpression : Expression, IPrintable
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this represents a root that is not a EntityQueryable, e.g. anonymous projection after Distinct.


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion
{
public class IncludeExpression : Expression, IPrintable
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we just produce IncludeMethod expression call instead? That's what this gets reduced to anyway

Copy link
Contributor

Choose a reason for hiding this comment

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

You can.


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion
{
public static class LinqMethodHelpers
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this might be useful in other places, not necessarily just for nav rewrite - move it to more common place?

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need actual method infos for queryable methods. We just need to know which method it is. A loose matching can easily tell that without much computation.
For Enumerable methods, we may need them for InMemory server side. But it should not be required in pre-compilation phase. If it is necessary then again loose match is pretty safe option.


In reply to: 276042273 [](ancestors = 276042273)


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion
{
public class NavigationBindingExpression : Expression, IPrintable
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this represents navigation property chains (or just "naked" parameter expression). As we expand the navigations, adding joins we keep updating source expression and the mapping. This way we don't need to re-create entire expression trees, but only mutate the mappings themselves. Once the given subexpression is processed, the binding gets rewritten to appropriate property access expressions which are immutable.


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion
{
public class NavigationExpansionExpression : Expression, IPrintable
Copy link
Contributor Author

Choose a reason for hiding this comment

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

represents IQueryable/IEnumerable that has it's navigations expanded. Operand is the actual expression produces so far (with the extra joins/groupjoins that were added), NavigationExpansionExpressionState holds all metadata we gathered along the way.

MaterializeCollectionNavigation = materializeCollectionNavigation;
}

public virtual ParameterExpression CurrentParameter { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

current parameter (either original or transparent identifier) for the IQueryable/IEnumerable. This should always be the parameter for the pending selector

}

public virtual ParameterExpression CurrentParameter { get; set; }
public virtual List<SourceMapping> SourceMappings { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

all the EntityQueryable sources found so far - there can be multiple sources due to join/groupjoin


public virtual ParameterExpression CurrentParameter { get; set; }
public virtual List<SourceMapping> SourceMappings { get; set; }
public virtual LambdaExpression PendingSelector { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

pending selector is what we need to apply to the NavigationExpansionExpression at the end so that the final shape matches the expected shape - it collapses all TransparentIdentifiers into actual entities etc

public virtual List<SourceMapping> SourceMappings { get; set; }
public virtual LambdaExpression PendingSelector { get; set; }
public virtual bool ApplyPendingSelector { get; set; }
public virtual List<(MethodInfo method, LambdaExpression keySelector)> PendingOrderings { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we need to apply orderings lazily because cases like: customers.OrderBy(c => c.Nav.Name).ThenBy(c => c.Id), if we apply first order by right away, we will introduce Join for the Nav expansion and lose the IOrderedQueryable type, which will break ThenBy. We need to apply Joins but hold off with the orderings until we encounter node that is NOT a ThenBy

public virtual LambdaExpression PendingSelector { get; set; }
public virtual bool ApplyPendingSelector { get; set; }
public virtual List<(MethodInfo method, LambdaExpression keySelector)> PendingOrderings { get; set; }
public virtual NavigationBindingExpression PendingIncludeChain { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this helps when processing ThenInclude - we want to know what we included before so we can build the full include chain easier

public virtual bool ApplyPendingSelector { get; set; }
public virtual List<(MethodInfo method, LambdaExpression keySelector)> PendingOrderings { get; set; }
public virtual NavigationBindingExpression PendingIncludeChain { get; set; }
public virtual MethodInfo PendingCardinalityReducingOperator { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FirstOrDefault, etc - this is applied lazily as well to help with member pushdown

public virtual NavigationBindingExpression PendingIncludeChain { get; set; }
public virtual MethodInfo PendingCardinalityReducingOperator { get; set; }
public virtual List<string> PendingTags { get; set; }
public virtual List<List<string>> CustomRootMappings { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

mappings for elements that are not EntityQueryables and therefore not part of SourceMapping structure - those are custom roots (e.g. anonymous projections after Distinct) and nested queryables, e.g. GroupJoin groupings or nested collections projections

public virtual MethodInfo PendingCardinalityReducingOperator { get; set; }
public virtual List<string> PendingTags { get; set; }
public virtual List<List<string>> CustomRootMappings { get; set; }
public virtual INavigation MaterializeCollectionNavigation { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

do we need to materialize a navigation of a given type - yes if "naked" collection navigation is projected, false otherwise

materializeCollectionNavigation?.ClrType ?? operand.Type);
}

public static (Expression source, ParameterExpression parameter) AddNavigationJoin(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

adds joins for all the reference navigations that were found but not expanded yet. It also changes all the mappings to new values

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: most of this code is copied from old nav rewrite, can probably be optimized


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion
{
public class NavigationExpansionRootExpression : Expression, IPrintable
Copy link
Contributor Author

Choose a reason for hiding this comment

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

represents a nested navigation expansion that needs it's own mapping to the root - e.g. for groupjoin scenarios, where the grouping is the Outer element of the Transparent identifier created in the GJ result selector


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion
{
public class NavigationTreeNode
Copy link
Contributor Author

Choose a reason for hiding this comment

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

represents the tree of navigations that were discovered so far as well as the mappings to and from. To mappings are way to locate the given navigation in the current transparent identifier (Outer/Inner props), From mappings are ways that the given navigation can be accessed originally. There can be multiple from mappings for a given navigation, because queries like these: customers.Select(c => new { one = c, two = c })

return result;
}

public List<NavigationTreeNode> Flatten()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

helper to give us all the children - this is mostly used to quickly check if some processing is needed in the tree, e.g. if there are any nodes with pending expansions

using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Extensions.Internal;

public class PendingSelectorIncludeRewriter : ExpressionVisitor
Copy link
Contributor Author

Choose a reason for hiding this comment

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

adds include methods to the final selector with the correct nesting of the include calls. e.g.

customers.Include(c => Foo).Include(c => Bar) will be rewritten to Include(Include(c, Foo), Bar).
customers.Include(c => Foo).ThenInclude(f => Bar) will be rewritten to Include(c, Include(f, Bar)),


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors
{
public class NavigationComparisonOptimizingVisitor : NavigationExpansionVisitorBase
Copy link
Contributor Author

Choose a reason for hiding this comment

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

rewrites nav comparison to key comparison instead


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors
{
public partial class NavigationExpandingVisitor : ExpressionVisitor
Copy link
Contributor Author

Choose a reason for hiding this comment

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

main navigation expansion processor - visits all the (currently supported) linq methods and converts them into NavigationExpansionExpressions, which can later be reduced into result expressions with their navigations expanded


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors
{
public partial class NavigationExpandingVisitor : ExpressionVisitor
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: will combine this with the file above - just split them temporarily

case "ThenInclude":
return ProcessInclude(methodCallExpression);

//TODO: should we have relational version of this? - probably
Copy link
Contributor Author

Choose a reason for hiding this comment

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

thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

We need relational version. We need to make it extensible so that providers can add custom queryable method processing


In reply to: 276049367 [](ancestors = 276049367)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will do that once we have the proper way to inject the visitors in the new pipeline

methodCallExpression.Type);
}

throw new InvalidOperationException("collection selector was not NavigationExpansionExpression");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: need to do something better here. we should always have NEE as a result of visiting SelectMany collection selector, unless query has some method that is not supported (maybe client method, or if we missed something)

// TODO: need to create the new state and copy includes from the old one, rather than simply copying it over to grouping
// this shouldn't be a problem currently since we don't support queries that compose on the grouping
// but when we do, state can't be shared - otherwise any nav expansion that affects the flattened part of the GroupJoin would be incorrectly propagated to the grouping as well
var newGrouping = new NavigationExpansionExpression(newGroupingParameter, innerApplyOrderingsResult.state, groupingParameter.Type);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in the second commit

return new NavigationExpansionExpression(rewritten, preProcessResult.state, methodCallExpression.Type);
}

private (Expression source, NavigationExpansionExpressionState state) PreProcessTerminatingOperation(NavigationExpansionExpression source)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

for terminating operators, like Distinct we need to "flush" all the accumulated stare - bring the result to the shape that is expected (i.e. get rid of TransparentIdentifiers) and then recreate new sources from the entities projected before the operator.

e.g. customers.Where(c => c.Nav.Name != "Foo").Select(c => new { foo = c, bar = c }.Distinct().Where(x => ...)
at the time of processing Where we will have two separate query sources for customer entity. We will also lose the navigation expanded in the first where, so if the second where accesses it again, it needs to be expanded the second time.

return new NavigationExpansionExpression(applyOrderingsResult.source, applyOrderingsResult.state, methodCallExpression.Type);
}

private MethodCallExpression SimplifyPredicateMethod(MethodCallExpression methodCallExpression, bool queryable)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

converts methods like entities.Count(x => x.Name != "Foo") into entities.Where(x => x.Name != "Foo").Count()

return (source, state);
}

private (Expression source, Expression lambdaBody, NavigationExpansionExpressionState state) FindAndApplyNavigations(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

takes a (single parameter) lambda from the queryable method that we are processing, finds all the new navigations and applies the joins for them

var pendingSelector = state.PendingSelector;
if (state.CurrentParameter != result.parameter)
{
var
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO - add tests for those, correct implementations is in the Include visitors, since logic is similar


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors
{
public class NavigationExpansionVisitorBase : ExpressionVisitor
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: convert this into visit children on individual expressions


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors
{
class NavigationPropertyBindingVisitor : NavigationExpansionVisitorBase
Copy link
Contributor Author

Choose a reason for hiding this comment

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

parses the property access chains and tries to match it with a NavigationTreeNode, based on nodes' FromMappings. If can't find a match it creates a new node at the correct place in the tree.


namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors
{
class NavigationPropertyUnbindingVisitor : NavigationExpansionVisitorBase
Copy link
Contributor Author

Choose a reason for hiding this comment

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

converts navigation bindings back into actual member access chains, also reduces navigation expansion expressions (in case of nested scenarios) - this is usually a final operation we do when processing lambda of a some query operator

namespace Microsoft.EntityFrameworkCore.Query.NavigationExpansion.Visitors
{

public class PendingIncludeFindingVisitor : ExpressionVisitor
Copy link
Contributor Author

Choose a reason for hiding this comment

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

finds all the pending includes that are actually accessible from the final projection - users could have included some navigations that are not accessible (e.g. behind scalar projection or not present at all) - we don't want to include those

@@ -5533,6 +5556,8 @@ public virtual Task SelectMany_navigation_property_followed_by_select_collection
[MemberData(nameof(IsAsyncData))]
public virtual Task Multiple_SelectMany_navigation_property_followed_by_select_collection_navigation(bool isAsync)
{
isAsync = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

will remove

}


//[ConditionalFact]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

forgot to remove

@@ -1960,7 +1984,7 @@ where e1.FirstName
entryCount: 1);
}

[ConditionalTheory(Skip = "Issue #14935. Cannot eval 'where ([e1].FirstName == {from Employee e in value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[Microsoft.EntityFrameworkCore.TestModels.Northwind.Employee]) orderby [e].EmployeeID asc select new <>f__AnonymousType334`1(Foo = [e]) => FirstOrDefault()}.Foo.FirstName)'")]
[ConditionalTheory]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

improvement!

@@ -62,7 +62,8 @@ public override async Task Key_equality_using_property_method_required2(bool isA
AssertSql(
@"SELECT [l].[Id], [l].[Date], [l].[Level1_Optional_Id], [l].[Level1_Required_Id], [l].[Name], [l].[OneToMany_Optional_Inverse2Id], [l].[OneToMany_Optional_Self_Inverse2Id], [l].[OneToMany_Required_Inverse2Id], [l].[OneToMany_Required_Self_Inverse2Id], [l].[OneToOne_Optional_PK_Inverse2Id], [l].[OneToOne_Optional_Self2Id]
FROM [LevelTwo] AS [l]
WHERE [l].[Level1_Required_Id] > 7");
INNER JOIN [LevelOne] AS [l.OneToOne_Required_FK_Inverse2] ON [l].[Level1_Required_Id] = [l.OneToOne_Required_FK_Inverse2].[Id]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we no longer do entity.Navigation.Pk -> entity.FK optimization, so there will be additional joins in some places

@maumar maumar force-pushed the nav_exp_pr_take1 branch 2 times, most recently from 935eb65 to 7000602 Compare April 19, 2019 01:22
@@ -107,6 +114,7 @@ public virtual TResult Execute<TResult>(Expression query)
var queryContext = _queryContextFactory.Create();

query = ExtractParameters(query, queryContext, _logger);
query = ExpandNavigations(query);
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be inside CompileQueryCore methods. You are expanding navigations for cached query too. That would be disastrous perf.

@maumar maumar force-pushed the nav_exp_pr_take1 branch from 7000602 to 708f48c Compare April 26, 2019 01:16
@smitpatel
Copy link
Contributor

:shipit:
We will merge this for now and look at any improvements combining with translation pipeline.

@maumar maumar force-pushed the nav_exp_pr_take1 branch from 708f48c to a18290d Compare April 30, 2019 00:21
Known limitations:
- GroupBy: not supported at all
- Include tracking/fixup: include method (one that suppose to do fixup and tracking) is just a stub currently - only does fixup one way and doesn't perform tracking at all,
- Various warnings and negative cases: we used to warn/throw particular exceptions for negative cases, those are now different and the messages themselves are not hardcoded and not polished
- include/project collection - correlated collection feature is disabled, so projecting collections will issue N+1 queries. Same goes for include collection.
- async + collections - no special handling of async, projecting/including collection may cause a deadlock
@maumar maumar force-pushed the nav_exp_pr_take1 branch from a18290d to 1dfb651 Compare April 30, 2019 00:23
@maumar maumar merged commit 1dfb651 into master Apr 30, 2019
@smitpatel smitpatel deleted the nav_exp_pr_take1 branch April 30, 2019 17:46
@roji
Copy link
Member

roji commented May 28, 2019

Note: this currently lives outside the Pipeline directory.

return ProcessMemberPushdown(newExpression, navigationExpansionExpression, efProperty: false, memberExpression.Member, propertyName: null, memberExpression.Type);
}

return base.VisitMember(memberExpression);
Copy link
Member

Choose a reason for hiding this comment

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

Aren't we visiting memberExpression.Expression twice in this method?

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

Successfully merging this pull request may close these issues.

3 participants