From ca206f33e3f9f1fa7eac97fce15e648136ef45e6 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Wed, 27 May 2020 11:32:40 -0700 Subject: [PATCH] Query: Introduce AsSplitQuery operator to run collection projection in separate command for relational Resolves #20892 --- ...ueryMetadataExtractingExpressionVisitor.cs | 1 - .../CosmosQueryTranslationPreprocessor.cs | 3 +- .../RelationalQueryableExtensions.cs | 31 +- ...ntityFrameworkRelationalServicesBuilder.cs | 4 +- ...CollectionJoinApplyingExpressionVisitor.cs | 15 +- .../Query/Internal/QueryingEnumerable.cs | 16 +- ...elationalQueryCompilationContextFactory.cs | 56 + ...ueryMetadataExtractingExpressionVisitor.cs | 54 + ...ext.cs => SingleQueryCollectionContext.cs} | 8 +- ...tor.cs => SingleQueryResultCoordinator.cs} | 12 +- .../SplitIncludeRewritingExpressionVisitor.cs | 172 +++ .../Internal/SplitQueryCollectionContext.cs | 61 + .../Internal/SplitQueryDataReaderContext.cs | 44 + .../Internal/SplitQueryResultCoordinator.cs | 87 ++ .../Query/Internal/SplitQueryingEnumerable.cs | 327 +++++ .../TableAliasUniquifyingExpressionVisitor.cs | 83 +- .../RelationalCollectionShaperExpression.cs | 1 - .../RelationalQueryCompilationContext.cs | 47 + ...onalQueryCompilationContextDependencies.cs | 58 + ...RelationalQueryTranslationPostprocessor.cs | 2 +- .../RelationalQueryTranslationPreprocessor.cs | 14 + ...sitor.ShaperProcessingExpressionVisitor.cs | 451 ++++-- ...alShapedQueryCompilingExpressionVisitor.cs | 33 +- ...lationalSplitCollectionShaperExpression.cs | 169 +++ .../Query/SqlExpressions/SelectExpression.cs | 200 ++- .../EntityFrameworkQueryableExtensions.cs | 208 +-- .../QueryCompilationContextFactory.cs | 3 + src/EFCore/Query/QueryCompilationContext.cs | 19 +- ...Core.Relational.Specification.Tests.csproj | 4 + .../NorthwindSplitIncludeQueryTestBase.cs | 138 ++ ...NorthwindSplitIncludeQuerySqlServerTest.cs | 1225 +++++++++++++++++ .../NorthwindSplitIncludeQuerySqliteTest.cs | 17 + 32 files changed, 3236 insertions(+), 327 deletions(-) create mode 100644 src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.cs create mode 100644 src/EFCore.Relational/Query/Internal/RelationalQueryMetadataExtractingExpressionVisitor.cs rename src/EFCore.Relational/Query/Internal/{CollectionMaterializationContext.cs => SingleQueryCollectionContext.cs} (96%) rename src/EFCore.Relational/Query/Internal/{ResultCoordinator.cs => SingleQueryResultCoordinator.cs} (90%) create mode 100644 src/EFCore.Relational/Query/Internal/SplitIncludeRewritingExpressionVisitor.cs create mode 100644 src/EFCore.Relational/Query/Internal/SplitQueryCollectionContext.cs create mode 100644 src/EFCore.Relational/Query/Internal/SplitQueryDataReaderContext.cs create mode 100644 src/EFCore.Relational/Query/Internal/SplitQueryResultCoordinator.cs create mode 100644 src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs create mode 100644 src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs create mode 100644 src/EFCore.Relational/Query/RelationalQueryCompilationContextDependencies.cs create mode 100644 src/EFCore.Relational/Query/RelationalSplitCollectionShaperExpression.cs create mode 100644 test/EFCore.Relational.Specification.Tests/Query/NorthwindSplitIncludeQueryTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSplitIncludeQuerySqliteTest.cs diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs index 1922169913e..0318c4ef57b 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryMetadataExtractingExpressionVisitor.cs @@ -49,6 +49,5 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return base.VisitMethodCall(methodCallExpression); } - } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs index f8396f30493..f68cd1b5ac2 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryTranslationPreprocessor.cs @@ -39,9 +39,8 @@ public CosmosQueryTranslationPreprocessor( /// public override Expression NormalizeQueryableMethod(Expression query) { - query = base.NormalizeQueryableMethod(query); - query = new CosmosQueryMetadataExtractingExpressionVisitor(_queryCompilationContext).Visit(query); + query = base.NormalizeQueryableMethod(query); return query; } diff --git a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs index a9abc8536a7..dd934e2e208 100644 --- a/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs @@ -6,6 +6,7 @@ using System.Data.Common; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; @@ -92,7 +93,7 @@ public static DbCommand CreateDbCommand([NotNull] this IQueryable source) [StringFormatMethod("sql")] public static IQueryable FromSqlRaw( [NotNull] this DbSet source, - [NotNull] [NotParameterized] string sql, + [NotNull][NotParameterized] string sql, [NotNull] params object[] parameters) where TEntity : class { @@ -133,7 +134,7 @@ public static IQueryable FromSqlRaw( /// An representing the interpolated string SQL query. public static IQueryable FromSqlInterpolated( [NotNull] this DbSet source, - [NotNull] [NotParameterized] FormattableString sql) + [NotNull][NotParameterized] FormattableString sql) where TEntity : class { Check.NotNull(source, nameof(source)); @@ -161,5 +162,31 @@ private static FromSqlQueryRootExpression GenerateFromSqlQueryRoot( sql, Expression.Constant(arguments)); } + + /// + /// + /// Returns a new query in which the collections in the query results will be loaded through separate database queries. + /// + /// + /// This strategy fetches all the data from the server through separate database queries before generating any results. + /// + /// + /// The type of entity being queried. + /// The source query. + /// A new query where collections will be loaded through separate database queries. + public static IQueryable AsSplitQuery( + [NotNull] this IQueryable source) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + return source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call(AsSplitQueryMethodInfo.MakeGenericMethod(typeof(TEntity)), source.Expression)) + : source; + } + + internal static readonly MethodInfo AsSplitQueryMethodInfo + = typeof(RelationalQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(AsSplitQuery)); } } diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 7138b7ec9b0..74b68930c77 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -169,6 +169,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); ServiceCollectionMap.GetInfrastructure() .AddDependencySingleton() @@ -201,7 +202,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() - .AddDependencyScoped(); + .AddDependencyScoped() + .AddDependencyScoped(); return base.TryAddCoreServices(); } diff --git a/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs index 35a73bcf4e1..b82f489f6fb 100644 --- a/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs @@ -15,8 +15,20 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal /// public class CollectionJoinApplyingExpressionVisitor : ExpressionVisitor { + private readonly bool _splitQuery; private int _collectionId; + /// + /// 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. + /// + public CollectionJoinApplyingExpressionVisitor(bool splitQuery) + { + _splitQuery = splitQuery; + } + /// /// 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 @@ -48,7 +60,8 @@ protected override Expression VisitExtension(Expression extensionExpression) collectionId, innerShaper, collectionShaperExpression.Navigation, - collectionShaperExpression.ElementType); + collectionShaperExpression.ElementType, + _splitQuery); } return extensionExpression is ShapedQueryExpression shapedQueryExpression diff --git a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs index 84e7d707bf0..6f83446ff73 100644 --- a/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/QueryingEnumerable.cs @@ -23,7 +23,7 @@ public class QueryingEnumerable : IEnumerable, IAsyncEnumerable, IRelat { private readonly RelationalQueryContext _relationalQueryContext; private readonly RelationalCommandCache _relationalCommandCache; - private readonly Func _shaper; + private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _performIdentityResolution; @@ -37,7 +37,7 @@ public class QueryingEnumerable : IEnumerable, IAsyncEnumerable, IRelat public QueryingEnumerable( [NotNull] RelationalQueryContext relationalQueryContext, [NotNull] RelationalCommandCache relationalCommandCache, - [NotNull] Func shaper, + [NotNull] Func shaper, [NotNull] Type contextType, bool performIdentityResolution) { @@ -106,13 +106,13 @@ private sealed class Enumerator : IEnumerator { private readonly RelationalQueryContext _relationalQueryContext; private readonly RelationalCommandCache _relationalCommandCache; - private readonly Func _shaper; + private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _performIdentityResolution; private RelationalDataReader _dataReader; - private ResultCoordinator _resultCoordinator; + private SingleQueryResultCoordinator _resultCoordinator; private IExecutionStrategy _executionStrategy; public Enumerator(QueryingEnumerable queryingEnumerable) @@ -200,7 +200,7 @@ private bool InitializeReader(DbContext _, bool result) _relationalQueryContext.Context, _relationalQueryContext.CommandLogger)); - _resultCoordinator = new ResultCoordinator(); + _resultCoordinator = new SingleQueryResultCoordinator(); _relationalQueryContext.InitializeStateManager(_performIdentityResolution); @@ -220,14 +220,14 @@ private sealed class AsyncEnumerator : IAsyncEnumerator { private readonly RelationalQueryContext _relationalQueryContext; private readonly RelationalCommandCache _relationalCommandCache; - private readonly Func _shaper; + private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _performIdentityResolution; private readonly CancellationToken _cancellationToken; private RelationalDataReader _dataReader; - private ResultCoordinator _resultCoordinator; + private SingleQueryResultCoordinator _resultCoordinator; private IExecutionStrategy _executionStrategy; public AsyncEnumerator( @@ -317,7 +317,7 @@ private async Task InitializeReaderAsync(DbContext _, bool result, Cancell _relationalQueryContext.CommandLogger), cancellationToken); - _resultCoordinator = new ResultCoordinator(); + _resultCoordinator = new SingleQueryResultCoordinator(); _relationalQueryContext.InitializeStateManager(_performIdentityResolution); diff --git a/src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.cs b/src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.cs new file mode 100644 index 00000000000..2023a6dcb1c --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalQueryCompilationContextFactory.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// + /// 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. + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + public class RelationalQueryCompilationContextFactory : IQueryCompilationContextFactory + { + private readonly QueryCompilationContextDependencies _dependencies; + private readonly RelationalQueryCompilationContextDependencies _relationalDependencies; + + /// + /// 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. + /// + public RelationalQueryCompilationContextFactory( + [NotNull] QueryCompilationContextDependencies dependencies, + [NotNull] RelationalQueryCompilationContextDependencies relationalDependencies) + { + Check.NotNull(dependencies, nameof(dependencies)); + Check.NotNull(relationalDependencies, nameof(relationalDependencies)); + + _dependencies = dependencies; + _relationalDependencies = relationalDependencies; + } + + /// + /// 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. + /// + public virtual QueryCompilationContext Create(bool async) + => new RelationalQueryCompilationContext(_dependencies, _relationalDependencies, async); + } +} + diff --git a/src/EFCore.Relational/Query/Internal/RelationalQueryMetadataExtractingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalQueryMetadataExtractingExpressionVisitor.cs new file mode 100644 index 00000000000..17d1657e930 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/RelationalQueryMetadataExtractingExpressionVisitor.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// 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. + /// + public class RelationalQueryMetadataExtractingExpressionVisitor : ExpressionVisitor + { + private readonly RelationalQueryCompilationContext _relationalQueryCompilationContext; + + /// + /// 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. + /// + public RelationalQueryMetadataExtractingExpressionVisitor([NotNull] RelationalQueryCompilationContext relationalQueryCompilationContext) + { + Check.NotNull(relationalQueryCompilationContext, nameof(relationalQueryCompilationContext)); + + _relationalQueryCompilationContext = relationalQueryCompilationContext; + } + + /// + /// 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. + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.GetGenericMethodDefinition() == RelationalQueryableExtensions.AsSplitQueryMethodInfo) + { + var innerQueryable = Visit(methodCallExpression.Arguments[0]); + + _relationalQueryCompilationContext.IsSplitQuery = true; + + return innerQueryable; + } + + return base.VisitMethodCall(methodCallExpression); + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/CollectionMaterializationContext.cs b/src/EFCore.Relational/Query/Internal/SingleQueryCollectionContext.cs similarity index 96% rename from src/EFCore.Relational/Query/Internal/CollectionMaterializationContext.cs rename to src/EFCore.Relational/Query/Internal/SingleQueryCollectionContext.cs index 8d9400ed7fe..79d197fba5a 100644 --- a/src/EFCore.Relational/Query/Internal/CollectionMaterializationContext.cs +++ b/src/EFCore.Relational/Query/Internal/SingleQueryCollectionContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using JetBrains.Annotations; @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal /// 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. /// - public class CollectionMaterializationContext + public class SingleQueryCollectionContext { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -19,8 +19,8 @@ public class CollectionMaterializationContext /// 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. /// - public CollectionMaterializationContext( - [NotNull] object parent, + public SingleQueryCollectionContext( + [CanBeNull] object parent, [NotNull] object collection, [NotNull] object[] parentIdentifier, [NotNull] object[] outerIdentifier) diff --git a/src/EFCore.Relational/Query/Internal/ResultCoordinator.cs b/src/EFCore.Relational/Query/Internal/SingleQueryResultCoordinator.cs similarity index 90% rename from src/EFCore.Relational/Query/Internal/ResultCoordinator.cs rename to src/EFCore.Relational/Query/Internal/SingleQueryResultCoordinator.cs index 4de31e2b75f..7896802af69 100644 --- a/src/EFCore.Relational/Query/Internal/ResultCoordinator.cs +++ b/src/EFCore.Relational/Query/Internal/SingleQueryResultCoordinator.cs @@ -12,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal /// 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. /// - public class ResultCoordinator + public class SingleQueryResultCoordinator { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -20,7 +20,7 @@ public class ResultCoordinator /// 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. /// - public ResultCoordinator() + public SingleQueryResultCoordinator() { ResultContext = new ResultContext(); } @@ -52,7 +52,7 @@ public ResultCoordinator() /// 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. /// - public virtual IList Collections { get; } = new List(); + public virtual IList Collections { get; } = new List(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -60,15 +60,15 @@ public ResultCoordinator() /// 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. /// - public virtual void SetCollectionMaterializationContext( - int collectionId, [NotNull] CollectionMaterializationContext collectionMaterializationContext) + public virtual void SetSingleQueryCollectionContext( + int collectionId, [NotNull] SingleQueryCollectionContext singleQueryCollectionContext) { while (Collections.Count <= collectionId) { Collections.Add(null); } - Collections[collectionId] = collectionMaterializationContext; + Collections[collectionId] = singleQueryCollectionContext; } } } diff --git a/src/EFCore.Relational/Query/Internal/SplitIncludeRewritingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SplitIncludeRewritingExpressionVisitor.cs new file mode 100644 index 00000000000..da4670399d8 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/SplitIncludeRewritingExpressionVisitor.cs @@ -0,0 +1,172 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// 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. + /// + public class SplitIncludeRewritingExpressionVisitor : ExpressionVisitor + { + /// + /// 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. + /// + public SplitIncludeRewritingExpressionVisitor() + { + } + + /// + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Method.DeclaringType == typeof(Queryable) + && methodCallExpression.Method.IsGenericMethod) + { + var genericMethod = methodCallExpression.Method.GetGenericMethodDefinition(); + Expression source = methodCallExpression; + var singleResult = false; + var reverseOrdering = false; + if (genericMethod == QueryableMethods.FirstOrDefaultWithoutPredicate + || genericMethod == QueryableMethods.FirstWithoutPredicate + || genericMethod == QueryableMethods.SingleOrDefaultWithoutPredicate + || genericMethod == QueryableMethods.SingleWithoutPredicate) + { + singleResult = true; + source = methodCallExpression.Arguments[0]; + } + + if (genericMethod == QueryableMethods.LastOrDefaultWithoutPredicate + || genericMethod == QueryableMethods.LastWithoutPredicate) + { + singleResult = true; + reverseOrdering = true; + source = methodCallExpression.Arguments[0]; + } + + if (source is MethodCallExpression selectMethodCall + && selectMethodCall.Method.DeclaringType == typeof(Queryable) + && selectMethodCall.Method.IsGenericMethod + && selectMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.Select) + { + var selector = RewriteCollectionInclude( + selectMethodCall.Arguments[0], selectMethodCall.Arguments[1].UnwrapLambdaFromQuote(), singleResult, reverseOrdering); + + source = selectMethodCall.Update( + selectMethodCall.Object, + new[] + { + selectMethodCall.Arguments[0], + Expression.Quote(selector) + }); + + return singleResult + ? methodCallExpression.Update(methodCallExpression.Object, new[] { source }) + : source; + } + } + + return base.VisitMethodCall(methodCallExpression); + } + + private LambdaExpression RewriteCollectionInclude(Expression source, LambdaExpression selector, bool singleResult, bool reverseOrdering) + { + var selectorParameter = selector.Parameters[0]; + var selectorBody = selector.Body; + var sourceElementType = source.Type.TryGetSequenceType(); + + if (reverseOrdering) + { + source = Expression.Call( + QueryableMethods.Reverse.MakeGenericMethod(sourceElementType), + source); + } + + if (singleResult) + { + source = Expression.Call( + QueryableMethods.Take.MakeGenericMethod(sourceElementType), + source, + Expression.Constant(1)); + } + + return Expression.Lambda( + new CollectionSelectManyInjectingExpressionVisitor(source, sourceElementType, selectorParameter).Visit(selectorBody), + selectorParameter); + } + + private class CollectionSelectManyInjectingExpressionVisitor : ExpressionVisitor + { + private readonly Expression _parentQuery; + private readonly Type _sourceElementType; + private readonly ParameterExpression _parameterExpression; + + public CollectionSelectManyInjectingExpressionVisitor( + Expression parentQuery, Type sourceElementType, ParameterExpression parameterExpression) + { + _parentQuery = new CloningExpressionVisitor().Visit(parentQuery); + _sourceElementType = sourceElementType; + _parameterExpression = parameterExpression; + } + + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is MaterializeCollectionNavigationExpression materializeCollectionNavigationExpression + && materializeCollectionNavigationExpression.Navigation.IsCollection) + { + var subquery = Visit(materializeCollectionNavigationExpression.Subquery); + var newParameter = Expression.Parameter(_parameterExpression.Type); + subquery = ReplacingExpressionVisitor.Replace(_parameterExpression, newParameter, subquery); + + // Collection selector body is IQueryable, we need to adjust the type to IEnumerable, to match the SelectMany signature + // therefore the delegate type is specified explicitly + var collectionElementType = subquery.Type.TryGetSequenceType(); + var collectionSelectorLambdaType = typeof(Func<,>).MakeGenericType( + _sourceElementType, + typeof(IEnumerable<>).MakeGenericType(collectionElementType)); + + subquery = Expression.Call( + QueryableMethods.SelectManyWithoutCollectionSelector.MakeGenericMethod(_sourceElementType, collectionElementType), + _parentQuery, + Expression.Quote(Expression.Lambda(collectionSelectorLambdaType, subquery, newParameter))); + + return materializeCollectionNavigationExpression.Update(subquery); + } + + return base.VisitExtension(extensionExpression); + } + + private class CloningExpressionVisitor : ExpressionVisitor + { + protected override Expression VisitLambda(Expression lambdaExpression) + { + var body = Visit(lambdaExpression.Body); + var newParameters = CopyParameters(lambdaExpression.Parameters); + body = new ReplacingExpressionVisitor(lambdaExpression.Parameters, newParameters).Visit(body); + + return lambdaExpression.Update(body, newParameters); + } + + private static IReadOnlyList CopyParameters(IReadOnlyList parameters) + { + var newParameters = new List(); + foreach (var parameter in parameters) + { + newParameters.Add(Expression.Parameter(parameter.Type, parameter.Name)); + } + + return newParameters; + } + } + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/SplitQueryCollectionContext.cs b/src/EFCore.Relational/Query/Internal/SplitQueryCollectionContext.cs new file mode 100644 index 00000000000..061ce23933a --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/SplitQueryCollectionContext.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// 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. + /// + public class SplitQueryCollectionContext + { + /// + /// 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. + /// + public SplitQueryCollectionContext( + [CanBeNull] object parent, + [NotNull] object collection, + [NotNull] object[] parentIdentifier) + { + Parent = parent; + Collection = collection; + ParentIdentifier = parentIdentifier; + ResultContext = new ResultContext(); + } + /// + /// 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. + /// + public virtual ResultContext ResultContext { get; } + /// + /// 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. + /// + public virtual object Parent { get; } + /// + /// 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. + /// + public virtual object Collection { get; } + /// + /// 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. + /// + public virtual object[] ParentIdentifier { get; } + } +} diff --git a/src/EFCore.Relational/Query/Internal/SplitQueryDataReaderContext.cs b/src/EFCore.Relational/Query/Internal/SplitQueryDataReaderContext.cs new file mode 100644 index 00000000000..e9c05e8bbea --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/SplitQueryDataReaderContext.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// 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. + /// + public class SplitQueryDataReaderContext + { + /// + /// 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. + /// + public SplitQueryDataReaderContext( + [NotNull] RelationalDataReader dataReader) + { + DataReader = dataReader; + } + + /// + /// 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. + /// + public virtual bool? HasNext { get; set; } + /// + /// 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. + /// + public virtual RelationalDataReader DataReader { get; } + } +} diff --git a/src/EFCore.Relational/Query/Internal/SplitQueryResultCoordinator.cs b/src/EFCore.Relational/Query/Internal/SplitQueryResultCoordinator.cs new file mode 100644 index 00000000000..ab46d32bbaa --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/SplitQueryResultCoordinator.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// 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. + /// + public class SplitQueryResultCoordinator + { + /// + /// 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. + /// + public SplitQueryResultCoordinator() + { + ResultContext = new ResultContext(); + } + + /// + /// 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. + /// + public virtual ResultContext ResultContext { get; } + + /// + /// 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. + /// + public virtual IList Collections { get; } = new List(); + + /// + /// 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. + /// + public virtual IList DataReaders { get; } = new List(); + + /// + /// 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. + /// + public virtual void SetDataReader( + int collectionId, [NotNull] RelationalDataReader relationalDataReader) + { + while (DataReaders.Count <= collectionId) + { + DataReaders.Add(null); + } + + DataReaders[collectionId] = new SplitQueryDataReaderContext(relationalDataReader); + } + + /// + /// 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. + /// + public virtual void SetSplitQueryCollectionContext( + int collectionId, [NotNull] SplitQueryCollectionContext splitQueryCollectionContext) + { + while (Collections.Count <= collectionId) + { + Collections.Add(null); + } + + Collections[collectionId] = splitQueryCollectionContext; + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs new file mode 100644 index 00000000000..da4155cc9f4 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs @@ -0,0 +1,327 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Query.Internal +{ + /// + /// 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. + /// + public class SplitQueryingEnumerable : IEnumerable, IAsyncEnumerable, IRelationalQueryingEnumerable + { + private readonly RelationalQueryContext _relationalQueryContext; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly Func _shaper; + private readonly Action _relatedDataLoaders; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _queryLogger; + private readonly bool _performIdentityResolution; + + /// + /// 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. + /// + public SplitQueryingEnumerable( + [NotNull] RelationalQueryContext relationalQueryContext, + [NotNull] RelationalCommandCache relationalCommandCache, + [NotNull] Func shaper, + [NotNull] Action relatedDataLoaders, + [NotNull] Type contextType, + bool performIdentityResolution) + { + _relationalQueryContext = relationalQueryContext; + _relationalCommandCache = relationalCommandCache; + _shaper = shaper; + _relatedDataLoaders = relatedDataLoaders; + _contextType = contextType; + _queryLogger = relationalQueryContext.QueryLogger; + _performIdentityResolution = performIdentityResolution; + } + + /// + /// 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. + /// + public virtual IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + => null; // new AsyncEnumerator(this, cancellationToken); + + /// + /// 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. + /// + public virtual IEnumerator GetEnumerator() => new Enumerator(this); + + /// + /// 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. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// 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. + /// + public virtual DbCommand CreateDbCommand() + => _relationalCommandCache + .GetRelationalCommand(_relationalQueryContext.ParameterValues) + .CreateDbCommand( + new RelationalCommandParameterObject( + _relationalQueryContext.Connection, + _relationalQueryContext.ParameterValues, + null, + null, + null), + Guid.Empty, + (DbCommandMethod)(-1)); + + /// + /// 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. + /// + public virtual string ToQueryString() + => _relationalQueryContext.RelationalQueryStringFactory.Create(CreateDbCommand()); + + private sealed class Enumerator : IEnumerator + { + private readonly RelationalQueryContext _relationalQueryContext; + private readonly RelationalCommandCache _relationalCommandCache; + private readonly Func _shaper; + private readonly Action _relatedDataLoaders; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _queryLogger; + private readonly bool _performIdentityResolution; + + private RelationalDataReader _dataReader; + private SplitQueryResultCoordinator _resultCoordinator; + private IExecutionStrategy _executionStrategy; + + public Enumerator(SplitQueryingEnumerable queryingEnumerable) + { + _relationalQueryContext = queryingEnumerable._relationalQueryContext; + _relationalCommandCache = queryingEnumerable._relationalCommandCache; + _shaper = queryingEnumerable._shaper; + _relatedDataLoaders = queryingEnumerable._relatedDataLoaders; + _contextType = queryingEnumerable._contextType; + _queryLogger = queryingEnumerable._queryLogger; + _performIdentityResolution = queryingEnumerable._performIdentityResolution; + } + + public T Current { get; private set; } + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + try + { + using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + { + if (_dataReader == null) + { + if (_executionStrategy == null) + { + _executionStrategy = _relationalQueryContext.ExecutionStrategyFactory.Create(); + } + + _executionStrategy.Execute(true, InitializeReader, null); + } + + var hasNext = _dataReader.Read(); + Current = default; + + if (hasNext) + { + _resultCoordinator.ResultContext.Values = null; + _shaper(_relationalQueryContext, _dataReader.DbDataReader, _resultCoordinator.ResultContext, _resultCoordinator); + _relatedDataLoaders(_relationalQueryContext, _resultCoordinator); + Current = _shaper( + _relationalQueryContext, _dataReader.DbDataReader, _resultCoordinator.ResultContext, _resultCoordinator); + } + + return hasNext; + } + } + catch (Exception exception) + { + _queryLogger.QueryIterationFailed(_contextType, exception); + + throw; + } + } + + private bool InitializeReader(DbContext _, bool result) + { + var relationalCommand = _relationalCommandCache.GetRelationalCommand(_relationalQueryContext.ParameterValues); + + _dataReader + = relationalCommand.ExecuteReader( + new RelationalCommandParameterObject( + _relationalQueryContext.Connection, + _relationalQueryContext.ParameterValues, + _relationalCommandCache.ReaderColumns, + _relationalQueryContext.Context, + _relationalQueryContext.CommandLogger)); + + _resultCoordinator = new SplitQueryResultCoordinator(); + + _relationalQueryContext.InitializeStateManager(_performIdentityResolution); + + return result; + } + + public void Dispose() + { + _dataReader?.Dispose(); + _dataReader = null; + } + + public void Reset() => throw new NotImplementedException(); + } + + //private sealed class AsyncEnumerator : IAsyncEnumerator + //{ + // private readonly RelationalQueryContext _relationalQueryContext; + // private readonly RelationalCommandCache _relationalCommandCache; + // private readonly Func _shaper; + // private readonly Type _contextType; + // private readonly IDiagnosticsLogger _queryLogger; + // private readonly bool _performIdentityResolution; + // private readonly CancellationToken _cancellationToken; + + // private RelationalDataReader _dataReader; + // private SplitQueryResultCoordinator _resultCoordinator; + // private IExecutionStrategy _executionStrategy; + + // public AsyncEnumerator( + // SplitQueryingEnumerable queryingEnumerable, + // CancellationToken cancellationToken) + // { + // _relationalQueryContext = queryingEnumerable._relationalQueryContext; + // _relationalCommandCache = queryingEnumerable._relationalCommandCache; + // _shaper = queryingEnumerable._shaper; + // _contextType = queryingEnumerable._contextType; + // _queryLogger = queryingEnumerable._queryLogger; + // _performIdentityResolution = queryingEnumerable._performIdentityResolution; + // _cancellationToken = cancellationToken; + // } + + // public T Current { get; private set; } + + // public async ValueTask MoveNextAsync() + // { + // try + // { + // using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + // { + // if (_dataReader == null) + // { + // if (_executionStrategy == null) + // { + // _executionStrategy = _relationalQueryContext.ExecutionStrategyFactory.Create(); + // } + + // await _executionStrategy.ExecuteAsync(true, InitializeReaderAsync, null, _cancellationToken); + // } + + // var hasNext = _resultCoordinator.HasNext ?? await _dataReader.ReadAsync(_cancellationToken); + // Current = default; + + // if (hasNext) + // { + // while (true) + // { + // _resultCoordinator.ResultReady = true; + // _resultCoordinator.HasNext = null; + // Current = _shaper( + // _relationalQueryContext, _dataReader.DbDataReader, _resultCoordinator.ResultContext, _resultCoordinator); + // if (_resultCoordinator.ResultReady) + // { + // // We generated a result so null out previously stored values + // _resultCoordinator.ResultContext.Values = null; + // break; + // } + + // if (!await _dataReader.ReadAsync(_cancellationToken)) + // { + // _resultCoordinator.HasNext = false; + // // Enumeration has ended, materialize last element + // _resultCoordinator.ResultReady = true; + // Current = _shaper( + // _relationalQueryContext, _dataReader.DbDataReader, _resultCoordinator.ResultContext, _resultCoordinator); + + // break; + // } + // } + // } + + // return hasNext; + // } + // } + // catch (Exception exception) + // { + // _queryLogger.QueryIterationFailed(_contextType, exception); + + // throw; + // } + // } + + // private async Task InitializeReaderAsync(DbContext _, bool result, CancellationToken cancellationToken) + // { + // var relationalCommand = _relationalCommandCache.GetRelationalCommand(_relationalQueryContext.ParameterValues); + + // _dataReader + // = await relationalCommand.ExecuteReaderAsync( + // new RelationalCommandParameterObject( + // _relationalQueryContext.Connection, + // _relationalQueryContext.ParameterValues, + // _relationalCommandCache.ReaderColumns, + // _relationalQueryContext.Context, + // _relationalQueryContext.CommandLogger), + // cancellationToken); + + // _resultCoordinator = new SplitQueryResultCoordinator(); + + // _relationalQueryContext.InitializeStateManager(_performIdentityResolution); + + // return result; + // } + + // public ValueTask DisposeAsync() + // { + // if (_dataReader != null) + // { + // var dataReader = _dataReader; + // _dataReader = null; + + // return dataReader.DisposeAsync(); + // } + + // return default; + // } + //} + } +} diff --git a/src/EFCore.Relational/Query/Internal/TableAliasUniquifyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/TableAliasUniquifyingExpressionVisitor.cs index f281e900d62..dc7c51c5f32 100644 --- a/src/EFCore.Relational/Query/Internal/TableAliasUniquifyingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/TableAliasUniquifyingExpressionVisitor.cs @@ -6,7 +6,6 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query.Internal { @@ -18,57 +17,71 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal /// public class TableAliasUniquifyingExpressionVisitor : ExpressionVisitor { - private readonly ISet _usedAliases = new HashSet(StringComparer.OrdinalIgnoreCase); - - private readonly ISet _visitedTableExpressionBases - = new HashSet(LegacyReferenceEqualityComparer.Instance); - /// /// 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. /// - protected override Expression VisitExtension(Expression extensionExpression) + public override Expression Visit(Expression expression) { - Check.NotNull(extensionExpression, nameof(extensionExpression)); + return expression is ShapedQueryExpression shapedQueryExpression + ? (Expression)shapedQueryExpression.Update( + UniquifyAliasInSelectExpression((SelectExpression)shapedQueryExpression.QueryExpression), + shapedQueryExpression.ShaperExpression) + : expression is RelationalSplitCollectionShaperExpression relationalSplitCollectionShaperExpression + ? relationalSplitCollectionShaperExpression.Update( + relationalSplitCollectionShaperExpression.ParentIdentifier, + relationalSplitCollectionShaperExpression.ChildIdentifier, + UniquifyAliasInSelectExpression(relationalSplitCollectionShaperExpression.SelectExpression), + relationalSplitCollectionShaperExpression.InnerShaper) + : base.Visit(expression); + } - if (extensionExpression is ShapedQueryExpression shapedQueryExpression) - { - return shapedQueryExpression.Update(Visit(shapedQueryExpression.QueryExpression), shapedQueryExpression.ShaperExpression); - } + private SelectExpression UniquifyAliasInSelectExpression(SelectExpression selectExpression) + => (SelectExpression)new ScopedVisitor().Visit(selectExpression); - var visitedExpression = base.VisitExtension(extensionExpression); - if (visitedExpression is TableExpressionBase tableExpressionBase - && !_visitedTableExpressionBases.Contains(tableExpressionBase) - && tableExpressionBase.Alias != null) - { - tableExpressionBase.Alias = GenerateUniqueAlias(tableExpressionBase.Alias); - _visitedTableExpressionBases.Add(tableExpressionBase); - } + private class ScopedVisitor : ExpressionVisitor + { + private readonly ISet _usedAliases = new HashSet(StringComparer.OrdinalIgnoreCase); - return visitedExpression; - } + private readonly ISet _visitedTableExpressionBases + = new HashSet(LegacyReferenceEqualityComparer.Instance); - private string GenerateUniqueAlias(string currentAlias) - { - if (!_usedAliases.Contains(currentAlias)) + public override Expression Visit(Expression expression) { - _usedAliases.Add(currentAlias); - return currentAlias; - } + var visitedExpression = base.Visit(expression); + if (visitedExpression is TableExpressionBase tableExpressionBase + && !_visitedTableExpressionBases.Contains(tableExpressionBase) + && tableExpressionBase.Alias != null) + { + tableExpressionBase.Alias = GenerateUniqueAlias(tableExpressionBase.Alias); + _visitedTableExpressionBases.Add(tableExpressionBase); + } - var counter = 0; - var uniqueAlias = currentAlias; + return visitedExpression; + } - while (_usedAliases.Contains(uniqueAlias)) + private string GenerateUniqueAlias(string currentAlias) { - uniqueAlias = currentAlias + counter++; - } + if (!_usedAliases.Contains(currentAlias)) + { + _usedAliases.Add(currentAlias); + return currentAlias; + } + + var counter = 0; + var uniqueAlias = currentAlias; - _usedAliases.Add(uniqueAlias); + while (_usedAliases.Contains(uniqueAlias)) + { + uniqueAlias = currentAlias + counter++; + } - return uniqueAlias; + _usedAliases.Add(uniqueAlias); + + return uniqueAlias; + } } } } diff --git a/src/EFCore.Relational/Query/RelationalCollectionShaperExpression.cs b/src/EFCore.Relational/Query/RelationalCollectionShaperExpression.cs index 87b3fe2b1d0..f91fd216e12 100644 --- a/src/EFCore.Relational/Query/RelationalCollectionShaperExpression.cs +++ b/src/EFCore.Relational/Query/RelationalCollectionShaperExpression.cs @@ -46,7 +46,6 @@ public RelationalCollectionShaperExpression( { } - /// /// Creates a new instance of the class. /// diff --git a/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs b/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs new file mode 100644 index 00000000000..536a32b8c87 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalQueryCompilationContext.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// The primary data structure representing the state/components used during relational query compilation. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public class RelationalQueryCompilationContext : QueryCompilationContext + { + /// + /// Creates a new instance of the class. + /// + /// Parameter object containing dependencies for this class. + /// Parameter object containing relational dependencies for this class. + /// A bool value indicating whether it is for async query. + public RelationalQueryCompilationContext( + [NotNull] QueryCompilationContextDependencies dependencies, + [NotNull] RelationalQueryCompilationContextDependencies relationalDependencies, + bool async) + : base (dependencies, async) + { + Check.NotNull(relationalDependencies, nameof(relationalDependencies)); + + RelationalDependencies = relationalDependencies; + } + + /// + /// Parameter object containing relational service dependencies. + /// + protected virtual RelationalQueryCompilationContextDependencies RelationalDependencies { get; } + + /// + /// A value indicating if the query should load collections using separate database queries. + /// + public virtual bool IsSplitQuery { get; internal set; } + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryCompilationContextDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryCompilationContextDependencies.cs new file mode 100644 index 00000000000..c0ace001fa7 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalQueryCompilationContextDependencies.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// Service dependencies parameter class for + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// Do not construct instances of this class directly from either provider or application code as the + /// constructor signature may change as new dependencies are added. Instead, use this type in + /// your constructor so that an instance will be created and injected automatically by the + /// dependency injection container. To create an instance with some dependent services replaced, + /// first resolve the object from the dependency injection container, then replace selected + /// services using the 'With...' methods. Do not call the constructor at any point in this process. + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + public sealed class RelationalQueryCompilationContextDependencies + { + /// + /// + /// Creates the service dependencies parameter object for a . + /// + /// + /// Do not call this constructor directly from either provider or application code as it may change + /// as new dependencies are added. Instead, use this type in your constructor so that an instance + /// will be created and injected automatically by the dependency injection container. To create + /// an instance with some dependent services replaced, first resolve the object from the dependency + /// injection container, then replace selected services using the 'With...' methods. Do not call + /// the constructor at any point in this process. + /// + /// + /// 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. + /// + /// + [EntityFrameworkInternal] + public RelationalQueryCompilationContextDependencies() + { + } + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs index 6f5d0ad52c4..6ca3e3ba183 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs @@ -40,7 +40,7 @@ public override Expression Process(Expression query) { query = base.Process(query); query = new SelectExpressionProjectionApplyingExpressionVisitor().Visit(query); - query = new CollectionJoinApplyingExpressionVisitor().Visit(query); + query = new CollectionJoinApplyingExpressionVisitor(((RelationalQueryCompilationContext)QueryCompilationContext).IsSplitQuery).Visit(query); query = new TableAliasUniquifyingExpressionVisitor().Visit(query); query = new CaseSimplifyingExpressionVisitor(RelationalDependencies.SqlExpressionFactory).Visit(query); diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs index c3468280c7c..3d57105ad74 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs @@ -11,6 +11,8 @@ namespace Microsoft.EntityFrameworkCore.Query /// public class RelationalQueryTranslationPreprocessor : QueryTranslationPreprocessor { + private readonly RelationalQueryCompilationContext _relationalQueryCompilationContext; + /// /// Creates a new instance of the class. /// @@ -26,6 +28,7 @@ public RelationalQueryTranslationPreprocessor( Check.NotNull(relationalDependencies, nameof(relationalDependencies)); RelationalDependencies = relationalDependencies; + _relationalQueryCompilationContext = (RelationalQueryCompilationContext)queryCompilationContext; } /// @@ -36,10 +39,21 @@ public RelationalQueryTranslationPreprocessor( /// public override Expression NormalizeQueryableMethod(Expression expression) { + expression = new RelationalQueryMetadataExtractingExpressionVisitor(_relationalQueryCompilationContext).Visit(expression); expression = base.NormalizeQueryableMethod(expression); expression = new TableValuedFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression); return expression; } + + /// + public override Expression Process(Expression query) + { + query = base.Process(query); + + return _relationalQueryCompilationContext.IsSplitQuery + ? new SplitIncludeRewritingExpressionVisitor().Visit(query) + : query; + } } } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 0e5e5a3bba2..16448af0fd6 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -9,6 +9,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -38,8 +39,8 @@ private sealed class ShaperProcessingExpressionVisitor : ExpressionVisitor private static readonly MemberInfo _resultContextValuesMemberInfo = typeof(ResultContext).GetMember(nameof(ResultContext.Values))[0]; - private static readonly MemberInfo _resultCoordinatorResultReadyMemberInfo - = typeof(ResultCoordinator).GetMember(nameof(ResultCoordinator.ResultReady))[0]; + private static readonly MemberInfo _singleQueryResultCoordinatorResultReadyMemberInfo + = typeof(SingleQueryResultCoordinator).GetMember(nameof(SingleQueryResultCoordinator.ResultReady))[0]; // Performing collection materialization private static readonly MethodInfo _includeReferenceMethodInfo @@ -48,6 +49,10 @@ private static readonly MethodInfo _initializeIncludeCollectionMethodInfo = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(InitializeIncludeCollection)); private static readonly MethodInfo _populateIncludeCollectionMethodInfo = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(PopulateIncludeCollection)); + private static readonly MethodInfo _initializeSplitIncludeCollectionMethodInfo + = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(InitializeSplitIncludeCollection)); + private static readonly MethodInfo _populateSplitIncludeCollectionMethodInfo + = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(PopulateSplitIncludeCollection)); private static readonly MethodInfo _initializeCollectionMethodInfo = typeof(ShaperProcessingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(InitializeCollection)); private static readonly MethodInfo _populateCollectionMethodInfo @@ -56,14 +61,16 @@ private static readonly MethodInfo _collectionAccessorAddMethodInfo = typeof(IClrCollectionAccessor).GetTypeInfo().GetDeclaredMethod(nameof(IClrCollectionAccessor.Add)); private readonly RelationalShapedQueryCompilingExpressionVisitor _parentVisitor; + private readonly bool _isTracking; + private readonly bool _splitQuery; private readonly bool _detailedErrorsEnabled; - private readonly bool _nested; + private readonly bool _separateCommandCache; + private readonly ParameterExpression _resultCoordinatorParameter; // States scoped to SelectExpression private readonly SelectExpression _selectExpression; private readonly ParameterExpression _dataReaderParameter; private readonly ParameterExpression _resultContextParameter; - private readonly ParameterExpression _resultCoordinatorParameter; private readonly ParameterExpression _indexMapParameter; private readonly ReaderColumn[] _readerColumns; @@ -89,100 +96,86 @@ private readonly IDictionary> _ public ShaperProcessingExpressionVisitor( RelationalShapedQueryCompilingExpressionVisitor parentVisitor, SelectExpression selectExpression, + bool splitQuery, bool indexMap) - : this(parentVisitor, selectExpression, - Expression.Parameter(typeof(DbDataReader), "dataReader"), - Expression.Parameter(typeof(ResultContext), "resultContext"), - Expression.Parameter(typeof(ResultCoordinator), "resultCoordinator"), - indexMap ? Expression.Parameter(typeof(int[]), "indexMap") : null, - parentVisitor.QueryCompilationContext.IsBuffering ? new ReaderColumn[selectExpression.Projection.Count] : null, - nested: false) { + _parentVisitor = parentVisitor; + _resultCoordinatorParameter = Expression.Parameter( + splitQuery ? typeof(SplitQueryResultCoordinator) : typeof(SingleQueryResultCoordinator), "resultCoordinator"); + + _selectExpression = selectExpression; + _dataReaderParameter = Expression.Parameter(typeof(DbDataReader), "dataReader"); + _resultContextParameter = Expression.Parameter(typeof(ResultContext), "resultContext"); + _indexMapParameter = indexMap ? Expression.Parameter(typeof(int[]), "indexMap") : null; + if (parentVisitor.QueryCompilationContext.IsBuffering) + { + _readerColumns = new ReaderColumn[_selectExpression.Projection.Count]; + } + + _separateCommandCache = true; + _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; + _isTracking = parentVisitor.QueryCompilationContext.IsTracking; + _splitQuery = splitQuery; } - // Private ctor to preserve states when needed for nested visitor - private ShaperProcessingExpressionVisitor( + // For single query scenario + public ShaperProcessingExpressionVisitor( RelationalShapedQueryCompilingExpressionVisitor parentVisitor, + ParameterExpression resultCoordinatorParameter, SelectExpression selectExpression, ParameterExpression dataReaderParameter, ParameterExpression resultContextParameter, - ParameterExpression resultCoordinatorParameter, - ParameterExpression indexMapParameter, - ReaderColumn[] readerColumns, - bool nested) + ReaderColumn[] readerColumns) { _parentVisitor = parentVisitor; + _resultCoordinatorParameter = resultCoordinatorParameter; + _selectExpression = selectExpression; _dataReaderParameter = dataReaderParameter; _resultContextParameter = resultContextParameter; - _resultCoordinatorParameter = resultCoordinatorParameter; - _indexMapParameter = indexMapParameter; _readerColumns = readerColumns; - _nested = nested; + _separateCommandCache = false; _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; + _isTracking = parentVisitor.QueryCompilationContext.IsTracking; + _splitQuery = false; } - public LambdaExpression ProcessShaper(Expression shaperExpression, out RelationalCommandCache relationalCommandCache) + // For split query scenario + public ShaperProcessingExpressionVisitor( + RelationalShapedQueryCompilingExpressionVisitor parentVisitor, + ParameterExpression resultCoordinatorParameter, + SelectExpression selectExpression) { - if (_indexMapParameter == null) - { - _containsCollectionMaterialization = new CollectionShaperFindingExpressionVisitor() - .ContainsCollectionMaterialization(shaperExpression); - } + _parentVisitor = parentVisitor; + _resultCoordinatorParameter = resultCoordinatorParameter; - if (_containsCollectionMaterialization) + _selectExpression = selectExpression; + _dataReaderParameter = Expression.Parameter(typeof(DbDataReader), "dataReader"); + _resultContextParameter = Expression.Parameter(typeof(ResultContext), "resultContext"); + if (parentVisitor.QueryCompilationContext.IsBuffering) { - _valuesArrayExpression = Expression.MakeMemberAccess(_resultContextParameter, _resultContextValuesMemberInfo); - _collectionPopulatingExpressions = new List(); - _valuesArrayInitializers = new List(); + _readerColumns = new ReaderColumn[_selectExpression.Projection.Count]; } + _separateCommandCache = true; + _detailedErrorsEnabled = parentVisitor._detailedErrorsEnabled; + _isTracking = parentVisitor.QueryCompilationContext.IsTracking; + _splitQuery = true; + } - var result = Visit(shaperExpression); - - if (_containsCollectionMaterialization) - { - var valueArrayInitializationExpression = Expression.Assign( - _valuesArrayExpression, - Expression.NewArrayInit( - typeof(object), - _valuesArrayInitializers)); - - _expressions.Add(valueArrayInitializationExpression); - _expressions.AddRange(_includeExpressions); - - var initializationBlock = Expression.Block( - _variables, - _expressions); - - var conditionalMaterializationExpressions = new List - { - Expression.IfThen( - Expression.Equal(_valuesArrayExpression, Expression.Constant(null, typeof(object[]))), - initializationBlock) - }; - - conditionalMaterializationExpressions.AddRange(_collectionPopulatingExpressions); - - conditionalMaterializationExpressions.Add( - Expression.Condition( - Expression.IsTrue( - Expression.MakeMemberAccess( - _resultCoordinatorParameter, _resultCoordinatorResultReadyMemberInfo)), - result, - Expression.Default(result.Type))); + public LambdaExpression ProcessShaper( + Expression shaperExpression, + out RelationalCommandCache relationalCommandCache, + out LambdaExpression relatedDataLoaders) + { + relatedDataLoaders = null; - result = Expression.Block(conditionalMaterializationExpressions); - } - else + if (_indexMapParameter != null) { - _expressions.AddRange(_includeExpressions); + var result = Visit(shaperExpression); _expressions.Add(result); result = Expression.Block(_variables, _expressions); - } - relationalCommandCache = _nested - ? null - : new RelationalCommandCache( + relationalCommandCache = new RelationalCommandCache( _parentVisitor.Dependencies.MemoryCache, _parentVisitor.RelationalDependencies.QuerySqlGeneratorFactory, _parentVisitor.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, @@ -190,18 +183,111 @@ public LambdaExpression ProcessShaper(Expression shaperExpression, out Relationa _readerColumns, _parentVisitor._useRelationalNulls); - return _indexMapParameter != null - ? Expression.Lambda( + return Expression.Lambda( + result, + QueryCompilationContext.QueryContextParameter, + _dataReaderParameter, + _indexMapParameter); + } + + _containsCollectionMaterialization = new CollectionShaperFindingExpressionVisitor() + .ContainsCollectionMaterialization(shaperExpression); + relatedDataLoaders = null; + + if (!_containsCollectionMaterialization) + { + var result = Visit(shaperExpression); + _expressions.AddRange(_includeExpressions); + _expressions.Add(result); + result = Expression.Block(_variables, _expressions); + + relationalCommandCache = _separateCommandCache + ? new RelationalCommandCache( + _parentVisitor.Dependencies.MemoryCache, + _parentVisitor.RelationalDependencies.QuerySqlGeneratorFactory, + _parentVisitor.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, + _selectExpression, + _readerColumns, + _parentVisitor._useRelationalNulls) + : null; + + return Expression.Lambda( result, QueryCompilationContext.QueryContextParameter, _dataReaderParameter, - _indexMapParameter) - : Expression.Lambda( + _resultContextParameter, + _resultCoordinatorParameter); + } + else + { + _valuesArrayExpression = Expression.MakeMemberAccess(_resultContextParameter, _resultContextValuesMemberInfo); + _collectionPopulatingExpressions = new List(); + _valuesArrayInitializers = new List(); + + var result = Visit(shaperExpression); + + var valueArrayInitializationExpression = Expression.Assign( + _valuesArrayExpression, Expression.NewArrayInit(typeof(object), _valuesArrayInitializers)); + + _expressions.Add(valueArrayInitializationExpression); + _expressions.AddRange(_includeExpressions); + + if (_splitQuery) + { + _expressions.Add(Expression.Default(result.Type)); + + var initializationBlock = Expression.Block(_variables, _expressions); + result = Expression.Condition( + Expression.Equal(_valuesArrayExpression, Expression.Constant(null, typeof(object[]))), + initializationBlock, + result); + + relatedDataLoaders = Expression.Lambda>( + Expression.Block(_collectionPopulatingExpressions), + QueryCompilationContext.QueryContextParameter, + _resultCoordinatorParameter); + } + else + { + var initializationBlock = Expression.Block(_variables, _expressions); + + var conditionalMaterializationExpressions = new List + { + Expression.IfThen( + Expression.Equal(_valuesArrayExpression, Expression.Constant(null, typeof(object[]))), + initializationBlock) + }; + + conditionalMaterializationExpressions.AddRange(_collectionPopulatingExpressions); + + conditionalMaterializationExpressions.Add( + Expression.Condition( + Expression.IsTrue( + Expression.MakeMemberAccess( + _resultCoordinatorParameter, _singleQueryResultCoordinatorResultReadyMemberInfo)), + result, + Expression.Default(result.Type))); + + result = Expression.Block(conditionalMaterializationExpressions); + } + + relationalCommandCache = _separateCommandCache + ? new RelationalCommandCache( + _parentVisitor.Dependencies.MemoryCache, + _parentVisitor.RelationalDependencies.QuerySqlGeneratorFactory, + _parentVisitor.RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, + _selectExpression, + _readerColumns, + _parentVisitor._useRelationalNulls) + : null; + + return Expression.Lambda( result, QueryCompilationContext.QueryContextParameter, _dataReaderParameter, _resultContextParameter, _resultCoordinatorParameter); + } } protected override Expression VisitBinary(BinaryExpression binaryExpression) @@ -342,9 +428,10 @@ protected override Expression VisitExtension(Expression extensionExpression) if (includeExpression.NavigationExpression is RelationalCollectionShaperExpression relationalCollectionShaperExpression) { - var innerShaper = new ShaperProcessingExpressionVisitor(_parentVisitor, _selectExpression, _dataReaderParameter, - _resultContextParameter, _resultCoordinatorParameter, null, _readerColumns, true) - .ProcessShaper(relationalCollectionShaperExpression.InnerShaper, out _); + var innerShaper = new ShaperProcessingExpressionVisitor( + _parentVisitor, _resultCoordinatorParameter, _selectExpression, _dataReaderParameter, _resultContextParameter, + _readerColumns) + .ProcessShaper(relationalCollectionShaperExpression.InnerShaper, out _, out _); var entityType = entity.Type; var navigation = includeExpression.Navigation; @@ -411,6 +498,74 @@ protected override Expression VisitExtension(Expression extensionExpression) includingEntityType, relatedEntityType, navigation, inverseNavigation).Compile()), Expression.Constant(_parentVisitor.QueryCompilationContext.IsTracking))); } + else if (includeExpression.NavigationExpression is RelationalSplitCollectionShaperExpression + relationalSplitCollectionShaperExpression) + { + var innerProcessor = new ShaperProcessingExpressionVisitor(_parentVisitor, _resultCoordinatorParameter, + relationalSplitCollectionShaperExpression.SelectExpression); + var innerShaper = innerProcessor.ProcessShaper(relationalSplitCollectionShaperExpression.InnerShaper, + out var relationalCommandCache, + out var relatedDataLoaders); + + var entityType = entity.Type; + var navigation = includeExpression.Navigation; + var includingEntityType = navigation.DeclaringEntityType.ClrType; + if (includingEntityType != entityType + && includingEntityType.IsAssignableFrom(entityType)) + { + includingEntityType = entityType; + } + + _inline = true; + + var parentIdentifierLambda = Expression.Lambda( + Visit(relationalSplitCollectionShaperExpression.ParentIdentifier), + QueryCompilationContext.QueryContextParameter, + _dataReaderParameter); + + _inline = false; + + innerProcessor._inline = true; + + var childIdentifierLambda = Expression.Lambda( + innerProcessor.Visit(relationalSplitCollectionShaperExpression.ChildIdentifier), + QueryCompilationContext.QueryContextParameter, + innerProcessor._dataReaderParameter); + + innerProcessor._inline = false; + + var collectionIdConstant = Expression.Constant(relationalSplitCollectionShaperExpression.CollectionId); + + _includeExpressions.Add(Expression.Call( + _initializeSplitIncludeCollectionMethodInfo.MakeGenericMethod(entityType, includingEntityType), + collectionIdConstant, + Expression.Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), + _dataReaderParameter, + _resultCoordinatorParameter, + Expression.Constant(relationalCommandCache), + entity, + Expression.Constant(parentIdentifierLambda.Compile()), + Expression.Constant(navigation), + Expression.Constant(navigation.GetCollectionAccessor()), + Expression.Constant(_isTracking))); + + var relatedEntityType = innerShaper.ReturnType; + var inverseNavigation = navigation.Inverse; + + _collectionPopulatingExpressions.Add(Expression.Call( + _populateSplitIncludeCollectionMethodInfo.MakeGenericMethod(includingEntityType, relatedEntityType), + collectionIdConstant, + QueryCompilationContext.QueryContextParameter, + _resultCoordinatorParameter, + Expression.Constant(childIdentifierLambda.Compile()), + Expression.Constant(relationalSplitCollectionShaperExpression.IdentifierValueComparers, typeof(IReadOnlyList)), + Expression.Constant(innerShaper.Compile()), + Expression.Constant(inverseNavigation, typeof(INavigation)), + Expression.Constant( + GenerateFixup( + includingEntityType, relatedEntityType, navigation, inverseNavigation).Compile()), + Expression.Constant(_isTracking))); + } else { var navigationExpression = Visit(includeExpression.NavigationExpression); @@ -448,9 +603,10 @@ protected override Expression VisitExtension(Expression extensionExpression) var key = GenerateKey(relationalCollectionShaperExpression); if (!_variableShaperMapping.TryGetValue(key, out var accessor)) { - var innerShaper = new ShaperProcessingExpressionVisitor(_parentVisitor, _selectExpression, _dataReaderParameter, - _resultContextParameter, _resultCoordinatorParameter, null, _readerColumns, true) - .ProcessShaper(relationalCollectionShaperExpression.InnerShaper, out _); + var innerShaper = new ShaperProcessingExpressionVisitor( + _parentVisitor, _resultCoordinatorParameter, _selectExpression, _dataReaderParameter, _resultContextParameter, + _readerColumns) + .ProcessShaper(relationalCollectionShaperExpression.InnerShaper, out _, out _); var collectionType = relationalCollectionShaperExpression.Type; var elementType = collectionType.TryGetSequenceType(); @@ -805,7 +961,7 @@ private static void InitializeIncludeCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, - ResultCoordinator resultCoordinator, + SingleQueryResultCoordinator resultCoordinator, TParent entity, Func parentIdentifier, Func outerIdentifier, @@ -832,23 +988,23 @@ private static void InitializeIncludeCollection( var parentKey = parentIdentifier(queryContext, dbDataReader); var outerKey = outerIdentifier(queryContext, dbDataReader); - var collectionMaterializationContext = new CollectionMaterializationContext(entity, collection, parentKey, outerKey); + var collectionMaterializationContext = new SingleQueryCollectionContext(entity, collection, parentKey, outerKey); - resultCoordinator.SetCollectionMaterializationContext(collectionId, collectionMaterializationContext); + resultCoordinator.SetSingleQueryCollectionContext(collectionId, collectionMaterializationContext); } private static void PopulateIncludeCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, - ResultCoordinator resultCoordinator, + SingleQueryResultCoordinator resultCoordinator, Func parentIdentifier, Func outerIdentifier, Func selfIdentifier, IReadOnlyList parentIdentifierValueComparers, IReadOnlyList outerIdentifierValueComparers, IReadOnlyList selfIdentifierValueComparers, - Func innerShaper, + Func innerShaper, INavigation inverseNavigation, Action fixup, bool trackingQuery) @@ -951,11 +1107,121 @@ void GenerateCurrentElementIfPending() } } + private static void InitializeSplitIncludeCollection( + int collectionId, + RelationalQueryContext queryContext, + DbDataReader parentDataReader, + SplitQueryResultCoordinator resultCoordinator, + RelationalCommandCache relationalCommandCache, + TParent entity, + Func parentIdentifier, + INavigation navigation, + IClrCollectionAccessor clrCollectionAccessor, + bool trackingQuery) + where TNavigationEntity : TParent + { + if (resultCoordinator.DataReaders.Count <= collectionId + || resultCoordinator.DataReaders[collectionId] == null) + { + // Execute and fetch data reader + RelationalDataReader dataReader = null; + queryContext.ExecutionStrategyFactory.Create().Execute(true, InitializeReader, null); + + bool InitializeReader(DbContext _, bool result) + { + var relationalCommand = relationalCommandCache.GetRelationalCommand(queryContext.ParameterValues); + + dataReader + = relationalCommand.ExecuteReader( + new RelationalCommandParameterObject( + queryContext.Connection, + queryContext.ParameterValues, + relationalCommandCache.ReaderColumns, + queryContext.Context, + queryContext.CommandLogger)); + + + return result; + } + + resultCoordinator.SetDataReader(collectionId, dataReader); + } + + object collection = null; + if (entity is TNavigationEntity) + { + if (trackingQuery) + { + queryContext.SetNavigationIsLoaded(entity, navigation); + } + else + { + SetIsLoadedNoTracking(entity, navigation); + } + + collection = clrCollectionAccessor.GetOrCreate(entity, forMaterialization: true); + } + + var parentKey = parentIdentifier(queryContext, parentDataReader); + + var splitQueryCollectionContext = new SplitQueryCollectionContext(entity, collection, parentKey); + + resultCoordinator.SetSplitQueryCollectionContext(collectionId, splitQueryCollectionContext); + } + + private static void PopulateSplitIncludeCollection( + int collectionId, + QueryContext queryContext, + SplitQueryResultCoordinator resultCoordinator, + Func childIdentifier, + IReadOnlyList identifierValueComparers, + Func innerShaper, + INavigation inverseNavigation, + Action fixup, + bool trackingQuery) + { + var splitQueryCollectionContext = resultCoordinator.Collections[collectionId]; + var dataReaderContext = resultCoordinator.DataReaders[collectionId]; + var dbDataReader = dataReaderContext.DataReader.DbDataReader; + if (splitQueryCollectionContext.Parent is TIncludingEntity entity) + { + while (dataReaderContext.HasNext ?? dbDataReader.Read()) + { + if (!CompareIdentifiers(identifierValueComparers, + splitQueryCollectionContext.ParentIdentifier, childIdentifier(queryContext, dbDataReader))) + { + dataReaderContext.HasNext = true; + + return; + } + + dataReaderContext.HasNext = null; + splitQueryCollectionContext.ResultContext.Values = null; + + innerShaper(queryContext, dbDataReader, splitQueryCollectionContext.ResultContext, resultCoordinator); + // Load nested collection + var relatedEntity = innerShaper( + queryContext, dbDataReader, splitQueryCollectionContext.ResultContext, resultCoordinator); + + if (!trackingQuery) + { + fixup(entity, relatedEntity); + if (inverseNavigation != null) + { + SetIsLoadedNoTracking(relatedEntity, inverseNavigation); + } + } + } + + dataReaderContext.HasNext = false; + } + } + private static TCollection InitializeCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, - ResultCoordinator resultCoordinator, + SingleQueryResultCoordinator resultCoordinator, Func parentIdentifier, Func outerIdentifier, IClrCollectionAccessor clrCollectionAccessor) @@ -966,9 +1232,9 @@ private static TCollection InitializeCollection( var parentKey = parentIdentifier(queryContext, dbDataReader); var outerKey = outerIdentifier(queryContext, dbDataReader); - var collectionMaterializationContext = new CollectionMaterializationContext(null, collection, parentKey, outerKey); + var collectionMaterializationContext = new SingleQueryCollectionContext(null, collection, parentKey, outerKey); - resultCoordinator.SetCollectionMaterializationContext(collectionId, collectionMaterializationContext); + resultCoordinator.SetSingleQueryCollectionContext(collectionId, collectionMaterializationContext); return (TCollection)collection; } @@ -977,14 +1243,14 @@ private static void PopulateCollection( int collectionId, QueryContext queryContext, DbDataReader dbDataReader, - ResultCoordinator resultCoordinator, + SingleQueryResultCoordinator resultCoordinator, Func parentIdentifier, Func outerIdentifier, Func selfIdentifier, IReadOnlyList parentIdentifierValueComparers, IReadOnlyList outerIdentifierValueComparers, IReadOnlyList selfIdentifierValueComparers, - Func innerShaper) + Func innerShaper) where TRelatedEntity : TElement where TCollection : class, ICollection { @@ -1132,7 +1398,8 @@ public override Expression Visit(Expression expression) return expression; } - if (expression is RelationalCollectionShaperExpression) + if (expression is RelationalCollectionShaperExpression + || expression is RelationalSplitCollectionShaperExpression) { _containsCollection = true; diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 0ee664cb590..e57a40a0cbe 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -59,11 +59,13 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery VerifyNoClientConstant(shapedQueryExpression.ShaperExpression); var nonComposedFromSql = selectExpression.IsNonComposedFromSql(); - var shaper = new ShaperProcessingExpressionVisitor(this, selectExpression, nonComposedFromSql).ProcessShaper( - shapedQueryExpression.ShaperExpression, out var relationalCommandCache); + var splitQuery = ((RelationalQueryCompilationContext)QueryCompilationContext).IsSplitQuery; + var shaper = new ShaperProcessingExpressionVisitor(this, selectExpression, splitQuery, nonComposedFromSql).ProcessShaper( + shapedQueryExpression.ShaperExpression, out var relationalCommandCache, out var relatedDataLoaders); - return nonComposedFromSql - ? Expression.New( + if (nonComposedFromSql) + { + return Expression.New( typeof(FromSqlQueryingEnumerable<>).MakeGenericType(shaper.ReturnType).GetConstructors()[0], Expression.Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), Expression.Constant(relationalCommandCache), @@ -71,8 +73,27 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery typeof(IReadOnlyList)), Expression.Constant(shaper.Compile()), Expression.Constant(_contextType), - Expression.Constant(base.QueryCompilationContext.PerformIdentityResolution)) - : Expression.New( + Expression.Constant(QueryCompilationContext.PerformIdentityResolution)); + } + + if (splitQuery) + { + if (QueryCompilationContext.IsAsync) + { + // + } + + return Expression.New( + typeof(SplitQueryingEnumerable<>).MakeGenericType(shaper.ReturnType).GetConstructors()[0], + Expression.Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), + Expression.Constant(relationalCommandCache), + Expression.Constant(shaper.Compile()), + Expression.Constant(relatedDataLoaders.Compile()), + Expression.Constant(_contextType), + Expression.Constant(QueryCompilationContext.PerformIdentityResolution)); + } + + return Expression.New( typeof(QueryingEnumerable<>).MakeGenericType(shaper.ReturnType).GetConstructors()[0], Expression.Convert(QueryCompilationContext.QueryContextParameter, typeof(RelationalQueryContext)), Expression.Constant(relationalCommandCache), diff --git a/src/EFCore.Relational/Query/RelationalSplitCollectionShaperExpression.cs b/src/EFCore.Relational/Query/RelationalSplitCollectionShaperExpression.cs new file mode 100644 index 00000000000..cd968adad71 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalSplitCollectionShaperExpression.cs @@ -0,0 +1,169 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// An expression that represents creation of a collection during split query for relational provider in . + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + public class RelationalSplitCollectionShaperExpression : Expression, IPrintableExpression + { + /// + /// Creates a new instance of the class. + /// + /// A unique id for the collection being shaped. + /// An identifier for the parent element. + /// An identifier for the child element. + /// A list of value comparers to compare identifiers. + /// A SQL query to get values for this collection from database. + /// An expression used to create individual elements of the collection. + /// A navigation associated with this collection, if any. + /// The clr type of individual elements in the collection. + public RelationalSplitCollectionShaperExpression( + int collectionId, + [NotNull] Expression parentIdentifier, + [NotNull] Expression childIdentifier, + [NotNull] IReadOnlyList identifierValueComparers, + [NotNull] SelectExpression selectExpression, + [NotNull] Expression innerShaper, + [CanBeNull] INavigation navigation, + [NotNull] Type elementType) + { + Check.NotNull(parentIdentifier, nameof(parentIdentifier)); + Check.NotNull(childIdentifier, nameof(childIdentifier)); + Check.NotEmpty(identifierValueComparers, nameof(identifierValueComparers)); + Check.NotNull(innerShaper, nameof(innerShaper)); + Check.NotNull(elementType, nameof(elementType)); + + CollectionId = collectionId; + ParentIdentifier = parentIdentifier; + ChildIdentifier = childIdentifier; + IdentifierValueComparers = identifierValueComparers; + SelectExpression = selectExpression; + InnerShaper = innerShaper; + Navigation = navigation; + ElementType = elementType; + } + + /// + /// A unique id for this collection shaper. + /// + public virtual int CollectionId { get; } + /// + /// The identifier for the parent element. + /// + public virtual Expression ParentIdentifier { get; } + /// + /// The identifier for the child element. + /// + public virtual Expression ChildIdentifier { get; } + /// + /// The list of value comparers to compare identifiers. + /// + public virtual IReadOnlyList IdentifierValueComparers { get; } + /// + /// The SQL query to get values for this collection from database. + /// + public virtual SelectExpression SelectExpression { get; } + + /// + /// The expression to create inner elements. + /// + public virtual Expression InnerShaper { get; } + /// + /// The navigation if associated with the collection. + /// + public virtual INavigation Navigation { get; } + /// + /// The clr type of elements of the collection. + /// + public virtual Type ElementType { get; } + + /// + public override Type Type => Navigation?.ClrType ?? typeof(List<>).MakeGenericType(ElementType); + /// + public sealed override ExpressionType NodeType => ExpressionType.Extension; + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + var parentIdentifier = visitor.Visit(ParentIdentifier); + var childIdentifier = visitor.Visit(ChildIdentifier); + var selectExpression = (SelectExpression)visitor.Visit(SelectExpression); + var innerShaper = visitor.Visit(InnerShaper); + + return Update(parentIdentifier, childIdentifier, selectExpression, innerShaper); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual RelationalSplitCollectionShaperExpression Update( + [NotNull] Expression parentIdentifier, + [NotNull] Expression childIdentifier, + [NotNull] SelectExpression selectExpression, + [NotNull] Expression innerShaper) + { + Check.NotNull(parentIdentifier, nameof(parentIdentifier)); + Check.NotNull(childIdentifier, nameof(childIdentifier)); + Check.NotNull(selectExpression, nameof(selectExpression)); + Check.NotNull(innerShaper, nameof(innerShaper)); + + return parentIdentifier != ParentIdentifier + || childIdentifier != ChildIdentifier + || selectExpression != SelectExpression + || innerShaper != InnerShaper + ? new RelationalSplitCollectionShaperExpression( + CollectionId, parentIdentifier, childIdentifier, IdentifierValueComparers, selectExpression, innerShaper, Navigation, ElementType) + : this; + } + + /// + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + { + Check.NotNull(expressionPrinter, nameof(expressionPrinter)); + + expressionPrinter.AppendLine("RelationalCollectionShaper:"); + using (expressionPrinter.Indent()) + { + expressionPrinter.AppendLine($"CollectionId: {CollectionId}"); + expressionPrinter.Append("ParentIdentifier:"); + expressionPrinter.Visit(ParentIdentifier); + expressionPrinter.AppendLine(); + expressionPrinter.Append("ChildIdentifier:"); + expressionPrinter.Visit(ChildIdentifier); + expressionPrinter.AppendLine(); + expressionPrinter.Append("SelectExpression:"); + expressionPrinter.Visit(SelectExpression); + expressionPrinter.AppendLine(); + expressionPrinter.Append("InnerShaper:"); + expressionPrinter.Visit(InnerShaper); + expressionPrinter.AppendLine(); + expressionPrinter.AppendLine($"Navigation: {Navigation?.Name}"); + } + } + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index cde4131a1e4..c7e2bd0e79b 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1043,7 +1043,9 @@ static Expression RemoveConvert(Expression expression) /// The type of the element in the collection. /// A which represents shaping of this collection. public CollectionShaperExpression AddCollectionProjection( - [NotNull] ShapedQueryExpression shapedQueryExpression, [CanBeNull] INavigation navigation, [CanBeNull] Type elementType) + [NotNull] ShapedQueryExpression shapedQueryExpression, + [CanBeNull] INavigation navigation, + [CanBeNull] Type elementType) { Check.NotNull(shapedQueryExpression, nameof(shapedQueryExpression)); @@ -1065,89 +1067,169 @@ public CollectionShaperExpression AddCollectionProjection( /// A shaper expression to use for shaping the elements of this collection. /// A navigation associated with this collection, if any. /// The type of the element in the collection. + /// A value indicating whether the collection query would be run with different DbCommand. /// An expression which represents shaping of this collection. public Expression ApplyCollectionJoin( int collectionIndex, int collectionId, [NotNull] Expression innerShaper, [CanBeNull] INavigation navigation, - [NotNull] Type elementType) + [NotNull] Type elementType, + bool splitQuery = false) { Check.NotNull(innerShaper, nameof(innerShaper)); Check.NotNull(elementType, nameof(elementType)); var innerSelectExpression = _pendingCollections[collectionIndex]; _pendingCollections[collectionIndex] = null; - var (parentIdentifier, parentIdentifierValueComparers) = GetIdentifierAccessor(_identifier); - var (outerIdentifier, outerIdentifierValueComparers) = GetIdentifierAccessor(_identifier.Concat(_childIdentifiers)); - innerSelectExpression.ApplyProjection(); - - var (selfIdentifier, selfIdentifierValueComparers) = innerSelectExpression.GetIdentifierAccessor(innerSelectExpression._identifier); - if (collectionIndex == 0) + if (splitQuery) { - foreach (var identifier in _identifier) + var parentIdentifier = GetIdentifierAccessor(_identifier).Item1; + //var (outerIdentifier, outerIdentifierValueComparers) = GetIdentifierAccessor(_identifier.Concat(_childIdentifiers)); + innerSelectExpression.ApplyProjection(); + var (childIdentifier, childIdentifierValueComparers) = innerSelectExpression + .GetIdentifierAccessor(innerSelectExpression._identifier.Take(_identifier.Count)); + + //if (collectionIndex == 0) + //{ + for (var i = 0; i < _identifier.Count; i++) { - AppendOrdering(new OrderingExpression(identifier.Column, ascending: true)); + AppendOrdering(new OrderingExpression(_identifier[i].Column, ascending: true)); + innerSelectExpression.AppendOrdering(new OrderingExpression(innerSelectExpression._identifier[i].Column, ascending: true)); } + //} + + //var joinPredicate = TryExtractJoinKey(innerSelectExpression); + //var containsOuterReference = new SelectExpressionCorrelationFindingExpressionVisitor(this) + // .ContainsOuterReference(innerSelectExpression); + //if (containsOuterReference && joinPredicate != null) + //{ + // innerSelectExpression.ApplyPredicate(joinPredicate); + // joinPredicate = null; + //} + + //if (innerSelectExpression.Offset != null + // || innerSelectExpression.Limit != null + // || innerSelectExpression.IsDistinct + // || innerSelectExpression.Predicate != null + // || innerSelectExpression.Tables.Count > 1 + // || innerSelectExpression.GroupBy.Count > 0) + //{ + // var sqlRemappingVisitor = new SqlRemappingVisitor( + // innerSelectExpression.PushdownIntoSubquery(), + // (SelectExpression)innerSelectExpression.Tables[0]); + // joinPredicate = sqlRemappingVisitor.Remap(joinPredicate); + //} + + //var joinExpression = joinPredicate == null + // ? (TableExpressionBase)new OuterApplyExpression(innerSelectExpression.Tables.Single()) + // : new LeftJoinExpression(innerSelectExpression.Tables.Single(), joinPredicate); + //_tables.Add(joinExpression); + + //foreach (var ordering in innerSelectExpression.Orderings) + //{ + // AppendOrdering(ordering.Update(MakeNullable(ordering.Expression))); + //} + + //var innerProjectionCount = innerSelectExpression.Projection.Count; + //var indexMap = new int[innerProjectionCount]; + //for (var i = 0; i < innerProjectionCount; i++) + //{ + // indexMap[i] = AddToProjection(MakeNullable(innerSelectExpression.Projection[i].Expression)); + //} + + //foreach (var identifier in innerSelectExpression._identifier.Concat(innerSelectExpression._childIdentifiers)) + //{ + // var updatedColumn = identifier.Column.MakeNullable(); + // _childIdentifiers.Add((updatedColumn, identifier.Comparer)); + // AppendOrdering(new OrderingExpression(updatedColumn, ascending: true)); + //} + + //// Inner should not have pendingCollection since we apply them first. + //// Shaper should not have CollectionShaperExpression as any collection would get converted to RelationalCollectionShaperExpression. + //var shaperRemapper = new ShaperRemappingExpressionVisitor(this, innerSelectExpression, indexMap, pendingCollectionOffset: 0); + //innerShaper = shaperRemapper.Visit(innerShaper); + //selfIdentifier = shaperRemapper.Visit(selfIdentifier); + + return new RelationalSplitCollectionShaperExpression( + collectionId, parentIdentifier, childIdentifier, childIdentifierValueComparers, + innerSelectExpression, innerShaper, navigation, elementType); } - - var joinPredicate = TryExtractJoinKey(innerSelectExpression); - var containsOuterReference = new SelectExpressionCorrelationFindingExpressionVisitor(this) - .ContainsOuterReference(innerSelectExpression); - if (containsOuterReference && joinPredicate != null) + else { - innerSelectExpression.ApplyPredicate(joinPredicate); - joinPredicate = null; - } - if (innerSelectExpression.Offset != null - || innerSelectExpression.Limit != null - || innerSelectExpression.IsDistinct - || innerSelectExpression.Predicate != null - || innerSelectExpression.Tables.Count > 1 - || innerSelectExpression.GroupBy.Count > 0) - { - var sqlRemappingVisitor = new SqlRemappingVisitor( - innerSelectExpression.PushdownIntoSubquery(), - (SelectExpression)innerSelectExpression.Tables[0]); - joinPredicate = sqlRemappingVisitor.Remap(joinPredicate); - } + var (parentIdentifier, parentIdentifierValueComparers) = GetIdentifierAccessor(_identifier); + var (outerIdentifier, outerIdentifierValueComparers) = GetIdentifierAccessor(_identifier.Concat(_childIdentifiers)); + innerSelectExpression.ApplyProjection(); - var joinExpression = joinPredicate == null - ? (TableExpressionBase)new OuterApplyExpression(innerSelectExpression.Tables.Single()) - : new LeftJoinExpression(innerSelectExpression.Tables.Single(), joinPredicate); - _tables.Add(joinExpression); + var (selfIdentifier, selfIdentifierValueComparers) = innerSelectExpression.GetIdentifierAccessor(innerSelectExpression._identifier); - foreach (var ordering in innerSelectExpression.Orderings) - { - AppendOrdering(ordering.Update(MakeNullable(ordering.Expression))); - } + if (collectionIndex == 0) + { + foreach (var identifier in _identifier) + { + AppendOrdering(new OrderingExpression(identifier.Column, ascending: true)); + } + } - var innerProjectionCount = innerSelectExpression.Projection.Count; - var indexMap = new int[innerProjectionCount]; - for (var i = 0; i < innerProjectionCount; i++) - { - indexMap[i] = AddToProjection(MakeNullable(innerSelectExpression.Projection[i].Expression)); - } + var joinPredicate = TryExtractJoinKey(innerSelectExpression); + var containsOuterReference = new SelectExpressionCorrelationFindingExpressionVisitor(this) + .ContainsOuterReference(innerSelectExpression); + if (containsOuterReference && joinPredicate != null) + { + innerSelectExpression.ApplyPredicate(joinPredicate); + joinPredicate = null; + } - foreach (var identifier in innerSelectExpression._identifier.Concat(innerSelectExpression._childIdentifiers)) - { - var updatedColumn = identifier.Column.MakeNullable(); - _childIdentifiers.Add((updatedColumn, identifier.Comparer)); - AppendOrdering(new OrderingExpression(updatedColumn, ascending: true)); - } + if (innerSelectExpression.Offset != null + || innerSelectExpression.Limit != null + || innerSelectExpression.IsDistinct + || innerSelectExpression.Predicate != null + || innerSelectExpression.Tables.Count > 1 + || innerSelectExpression.GroupBy.Count > 0) + { + var sqlRemappingVisitor = new SqlRemappingVisitor( + innerSelectExpression.PushdownIntoSubquery(), + (SelectExpression)innerSelectExpression.Tables[0]); + joinPredicate = sqlRemappingVisitor.Remap(joinPredicate); + } - // Inner should not have pendingCollection since we apply them first. - // Shaper should not have CollectionShaperExpression as any collection would get converted to RelationalCollectionShaperExpression. - var shaperRemapper = new ShaperRemappingExpressionVisitor(this, innerSelectExpression, indexMap, pendingCollectionOffset: 0); - innerShaper = shaperRemapper.Visit(innerShaper); - selfIdentifier = shaperRemapper.Visit(selfIdentifier); + var joinExpression = joinPredicate == null + ? (TableExpressionBase)new OuterApplyExpression(innerSelectExpression.Tables.Single()) + : new LeftJoinExpression(innerSelectExpression.Tables.Single(), joinPredicate); + _tables.Add(joinExpression); - return new RelationalCollectionShaperExpression( - collectionId, parentIdentifier, outerIdentifier, selfIdentifier, - parentIdentifierValueComparers, outerIdentifierValueComparers, selfIdentifierValueComparers, - innerShaper, navigation, elementType); + foreach (var ordering in innerSelectExpression.Orderings) + { + AppendOrdering(ordering.Update(MakeNullable(ordering.Expression))); + } + + var innerProjectionCount = innerSelectExpression.Projection.Count; + var indexMap = new int[innerProjectionCount]; + for (var i = 0; i < innerProjectionCount; i++) + { + indexMap[i] = AddToProjection(MakeNullable(innerSelectExpression.Projection[i].Expression)); + } + + foreach (var identifier in innerSelectExpression._identifier.Concat(innerSelectExpression._childIdentifiers)) + { + var updatedColumn = identifier.Column.MakeNullable(); + _childIdentifiers.Add((updatedColumn, identifier.Comparer)); + AppendOrdering(new OrderingExpression(updatedColumn, ascending: true)); + } + + // Inner should not have pendingCollection since we apply them first. + // Shaper should not have CollectionShaperExpression as any collection would get converted to RelationalCollectionShaperExpression. + var shaperRemapper = new ShaperRemappingExpressionVisitor(this, innerSelectExpression, indexMap, pendingCollectionOffset: 0); + innerShaper = shaperRemapper.Visit(innerShaper); + selfIdentifier = shaperRemapper.Visit(selfIdentifier); + + return new RelationalCollectionShaperExpression( + collectionId, parentIdentifier, outerIdentifier, selfIdentifier, + parentIdentifierValueComparers, outerIdentifierValueComparers, selfIdentifierValueComparers, + innerShaper, navigation, elementType); + } } private static SqlExpression MakeNullable(SqlExpression sqlExpression) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 2154059af6a..e5c4edad448 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -35,7 +35,7 @@ public static class EntityFrameworkQueryableExtensions /// /// The query source. /// The query string for debugging. - /// + /// /// is . /// public static string ToQueryString([NotNull] this IQueryable source) @@ -69,7 +69,7 @@ public static string ToQueryString([NotNull] this IQueryable source) /// A task that represents the asynchronous operation. /// The task result contains if the source sequence contains any elements; otherwise, . /// - /// + /// /// is . /// public static Task AnyAsync( @@ -103,7 +103,7 @@ public static Task AnyAsync( /// The task result contains if any elements in the source sequence pass the test in the specified /// predicate; otherwise, . /// - /// + /// /// or is . /// public static Task AnyAsync( @@ -139,7 +139,7 @@ public static Task AnyAsync( /// The task result contains if every element of the source sequence passes the test in the specified /// predicate; otherwise, . /// - /// + /// /// or is . /// public static Task AllAsync( @@ -177,7 +177,7 @@ public static Task AllAsync( /// A task that represents the asynchronous operation. /// The task result contains the number of elements in the input sequence. /// - /// + /// /// is . /// public static Task CountAsync( @@ -211,7 +211,7 @@ public static Task CountAsync( /// The task result contains the number of elements in the sequence that satisfy the condition in the predicate /// function. /// - /// + /// /// or is . /// public static Task CountAsync( @@ -245,7 +245,7 @@ public static Task CountAsync( /// A task that represents the asynchronous operation. /// The task result contains the number of elements in the input sequence. /// - /// + /// /// is . /// public static Task LongCountAsync( @@ -280,7 +280,7 @@ public static Task LongCountAsync( /// The task result contains the number of elements in the sequence that satisfy the condition in the predicate /// function. /// - /// + /// /// or is . /// public static Task LongCountAsync( @@ -318,10 +318,10 @@ public static Task LongCountAsync( /// A task that represents the asynchronous operation. /// The task result contains the first element in . /// - /// + /// /// is . /// - /// + /// /// contains no elements. /// public static Task FirstAsync( @@ -355,10 +355,10 @@ public static Task FirstAsync( /// The task result contains the first element in that passes the test in /// . /// - /// + /// /// or is . /// - /// + /// /// /// No element satisfies the condition in /// @@ -401,7 +401,7 @@ public static Task FirstAsync( /// The task result contains ( ) if /// is empty; otherwise, the first element in . /// - /// + /// /// is . /// public static Task FirstOrDefaultAsync( @@ -437,7 +437,7 @@ public static Task FirstOrDefaultAsync( /// is empty or if no element passes the test specified by ; otherwise, the first /// element in that passes the test specified by . /// - /// + /// /// or is . /// public static Task FirstOrDefaultAsync( @@ -475,10 +475,10 @@ public static Task FirstOrDefaultAsync( /// A task that represents the asynchronous operation. /// The task result contains the last element in . /// - /// + /// /// is . /// - /// + /// /// contains no elements. /// public static Task LastAsync( @@ -512,10 +512,10 @@ public static Task LastAsync( /// The task result contains the last element in that passes the test in /// . /// - /// + /// /// or is . /// - /// + /// /// /// No element satisfies the condition in . /// @@ -558,7 +558,7 @@ public static Task LastAsync( /// The task result contains ( ) if /// is empty; otherwise, the last element in . /// - /// + /// /// is . /// public static Task LastOrDefaultAsync( @@ -594,7 +594,7 @@ public static Task LastOrDefaultAsync( /// is empty or if no element passes the test specified by ; otherwise, the last /// element in that passes the test specified by . /// - /// + /// /// or is . /// public static Task LastOrDefaultAsync( @@ -633,10 +633,10 @@ public static Task LastOrDefaultAsync( /// A task that represents the asynchronous operation. /// The task result contains the single element of the input sequence. /// - /// + /// /// is . /// - /// + /// /// /// contains more than one elements. /// @@ -679,10 +679,10 @@ public static Task SingleAsync( /// The task result contains the single element of the input sequence that satisfies the condition in /// . /// - /// + /// /// or is . /// - /// + /// /// /// No element satisfies the condition in . /// @@ -733,10 +733,10 @@ public static Task SingleAsync( /// ) /// if the sequence contains no elements. /// - /// + /// /// is . /// - /// + /// /// contains more than one element. /// public static Task SingleOrDefaultAsync( @@ -772,10 +772,10 @@ public static Task SingleOrDefaultAsync( /// The task result contains the single element of the input sequence that satisfies the condition in /// , or ( ) if no such element is found. /// - /// + /// /// or is . /// - /// + /// /// More than one element satisfies the condition in . /// public static Task SingleOrDefaultAsync( @@ -814,7 +814,7 @@ public static Task SingleOrDefaultAsync( /// A task that represents the asynchronous operation. /// The task result contains the minimum value in the sequence. /// - /// + /// /// is . /// public static Task MinAsync( @@ -850,7 +850,7 @@ public static Task MinAsync( /// A task that represents the asynchronous operation. /// The task result contains the minimum value in the sequence. /// - /// + /// /// or is . /// public static Task MinAsync( @@ -888,7 +888,7 @@ public static Task MinAsync( /// A task that represents the asynchronous operation. /// The task result contains the maximum value in the sequence. /// - /// + /// /// is . /// public static Task MaxAsync( @@ -924,7 +924,7 @@ public static Task MaxAsync( /// A task that represents the asynchronous operation. /// The task result contains the maximum value in the sequence. /// - /// + /// /// or is . /// public static Task MaxAsync( @@ -959,7 +959,7 @@ public static Task MaxAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -988,7 +988,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1020,7 +1020,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1054,7 +1054,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1086,7 +1086,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1115,7 +1115,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1146,7 +1146,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1179,7 +1179,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1211,7 +1211,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1240,7 +1240,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1271,7 +1271,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1305,7 +1305,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1337,7 +1337,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1366,7 +1366,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1397,7 +1397,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1431,7 +1431,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1463,7 +1463,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1492,7 +1492,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the values in the sequence. /// - /// + /// /// is . /// public static Task SumAsync( @@ -1523,7 +1523,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1557,7 +1557,7 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the sum of the projected values.. /// - /// + /// /// or is . /// public static Task SumAsync( @@ -1593,10 +1593,10 @@ public static Task SumAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -1626,7 +1626,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// public static Task AverageAsync( @@ -1659,10 +1659,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -1697,7 +1697,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// public static Task AverageAsync( @@ -1729,10 +1729,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -1761,7 +1761,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// public static Task AverageAsync( @@ -1793,10 +1793,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -1831,7 +1831,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// public static Task AverageAsync( @@ -1863,10 +1863,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -1895,7 +1895,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// public static Task AverageAsync( @@ -1927,10 +1927,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -1965,7 +1965,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// public static Task AverageAsync( @@ -1997,10 +1997,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -2030,7 +2030,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// public static Task AverageAsync( @@ -2063,10 +2063,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -2101,7 +2101,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// public static Task AverageAsync( @@ -2133,10 +2133,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -2165,7 +2165,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the sequence of values. /// - /// + /// /// is . /// public static Task AverageAsync( @@ -2198,10 +2198,10 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// - /// + /// /// contains no elements. /// public static Task AverageAsync( @@ -2236,7 +2236,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains the average of the projected values. /// - /// + /// /// or is . /// public static Task AverageAsync( @@ -2276,7 +2276,7 @@ public static Task AverageAsync( /// A task that represents the asynchronous operation. /// The task result contains if the input sequence contains the specified value; otherwise, . /// - /// + /// /// is . /// public static Task ContainsAsync( @@ -2318,7 +2318,7 @@ public static Task ContainsAsync( /// A task that represents the asynchronous operation. /// The task result contains a that contains elements from the input sequence. /// - /// + /// /// is . /// public static async Task> ToListAsync( @@ -2354,7 +2354,7 @@ public static async Task> ToListAsync( /// A task that represents the asynchronous operation. /// The task result contains an array that contains elements from the input sequence. /// - /// + /// /// is . /// public static async Task ToArrayAsync( @@ -2421,7 +2421,7 @@ internal static readonly MethodInfo IncludeMethodInfo /// /// A new query with the related data included. /// - /// + /// /// or is . /// public static IIncludableQueryable Include( @@ -2627,10 +2627,10 @@ internal static readonly MethodInfo StringIncludeMethodInfo /// The source query. /// A string of '.' separated navigation property names to be included. /// A new query with the related data included. - /// + /// /// or is . /// - /// + /// /// is empty or whitespace. /// public static IQueryable Include( @@ -2669,7 +2669,7 @@ internal static readonly MethodInfo IgnoreQueryFiltersMethodInfo /// /// A new query that will not apply any model-level entity query filters. /// - /// + /// /// is . /// public static IQueryable IgnoreQueryFilters( @@ -2717,7 +2717,7 @@ internal static readonly MethodInfo AsNoTrackingMethodInfo /// /// A new query where the result set will not be tracked by the context. /// - /// + /// /// is . /// public static IQueryable AsNoTracking( @@ -2757,7 +2757,7 @@ internal static readonly MethodInfo AsTrackingMethodInfo /// /// A new query where the result set will be tracked by the context. /// - /// + /// /// is . /// public static IQueryable AsTracking( @@ -2801,7 +2801,7 @@ source.Provider is EntityQueryProvider /// /// A new query where the result set will be tracked by the context. /// - /// + /// /// is . /// public static IQueryable AsTracking( @@ -2834,7 +2834,7 @@ internal static readonly MethodInfo PerformIdentityResolutionMethodInfo /// /// A new query where the result set will not be tracked by the context. /// - /// + /// /// is . /// public static IQueryable PerformIdentityResolution( @@ -2871,10 +2871,10 @@ internal static readonly MethodInfo TagWithMethodInfo /// /// A new query annotated with the given tag. /// - /// + /// /// or is . /// - /// + /// /// is empty or whitespace. /// public static IQueryable TagWith( @@ -2904,7 +2904,7 @@ source.Provider is EntityQueryProvider /// and then throwing away the list (without the overhead of actually creating the list). /// /// The source query. - /// + /// /// is . /// public static void Load([NotNull] this IQueryable source) @@ -2927,7 +2927,7 @@ public static void Load([NotNull] this IQueryable source) /// A to observe while waiting for the task to complete. /// /// A task that represents the asynchronous operation. - /// + /// /// is . /// public static async Task LoadAsync( @@ -2973,7 +2973,7 @@ public static async Task LoadAsync( /// A task that represents the asynchronous operation. /// The task result contains a that contains selected keys and values. /// - /// + /// /// or is . /// public static Task> ToDictionaryAsync( @@ -3011,7 +3011,7 @@ public static Task> ToDictionaryAsync( /// A task that represents the asynchronous operation. /// The task result contains a that contains selected keys and values. /// - /// + /// /// or is . /// public static Task> ToDictionaryAsync( @@ -3052,7 +3052,7 @@ public static Task> ToDictionaryAsync( /// The task result contains a that contains values of type /// selected from the input sequence. /// - /// + /// /// or or is . /// public static Task> ToDictionaryAsync( @@ -3096,7 +3096,7 @@ public static Task> ToDictionaryAsync that contains values of type /// selected from the input sequence. /// - /// + /// /// or or is . /// public static async Task> ToDictionaryAsync( @@ -3141,7 +3141,7 @@ public static async Task> ToDictionaryAsync to observe while waiting for the task to complete. /// /// A task that represents the asynchronous operation. - /// + /// /// or is . /// public static async Task ForEachAsync( @@ -3175,10 +3175,10 @@ public static async Task ForEachAsync( /// An to enumerate. /// /// The query results. - /// + /// /// is . /// - /// + /// /// is not a . /// public static IAsyncEnumerable AsAsyncEnumerable( diff --git a/src/EFCore/Query/Internal/QueryCompilationContextFactory.cs b/src/EFCore/Query/Internal/QueryCompilationContextFactory.cs index fbf0b76c029..939f07d5c52 100644 --- a/src/EFCore/Query/Internal/QueryCompilationContextFactory.cs +++ b/src/EFCore/Query/Internal/QueryCompilationContextFactory.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore.Query.Internal @@ -32,6 +33,8 @@ public class QueryCompilationContextFactory : IQueryCompilationContextFactory /// public QueryCompilationContextFactory([NotNull] QueryCompilationContextDependencies dependencies) { + Check.NotNull(dependencies, nameof(dependencies)); + _dependencies = dependencies; } diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs index f677184b008..10596062119 100644 --- a/src/EFCore/Query/QueryCompilationContext.cs +++ b/src/EFCore/Query/QueryCompilationContext.cs @@ -15,8 +15,13 @@ namespace Microsoft.EntityFrameworkCore.Query { /// - /// A query compilation context. The primary data structure representing the state/components - /// used during query compilation. + /// + /// The primary data structure representing the state/components used during query compilation. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// /// public class QueryCompilationContext { @@ -50,9 +55,9 @@ public class QueryCompilationContext private Dictionary _runtimeParameters; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// - /// Parameter object containing dependencies for this convention. + /// Parameter object containing dependencies for this class. /// A bool value indicating whether it is for async query. public QueryCompilationContext( [NotNull] QueryCompilationContextDependencies dependencies, @@ -60,6 +65,7 @@ public QueryCompilationContext( { Check.NotNull(dependencies, nameof(dependencies)); + Dependencies = dependencies; IsAsync = async; IsTracking = dependencies.IsTracking; IsBuffering = dependencies.IsRetryingExecutionStrategy; @@ -74,6 +80,11 @@ public QueryCompilationContext( _shapedQueryCompilingExpressionVisitorFactory = dependencies.ShapedQueryCompilingExpressionVisitorFactory; } + /// + /// Parameter object containing dependencies for this service. + /// + protected virtual QueryCompilationContextDependencies Dependencies { get; } + /// /// A value indicating whether it is async query. /// diff --git a/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj b/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj index e27c5cebb69..104d737e3f2 100644 --- a/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj +++ b/test/EFCore.Relational.Specification.Tests/EFCore.Relational.Specification.Tests.csproj @@ -11,6 +11,10 @@ true + + + + diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindSplitIncludeQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSplitIncludeQueryTestBase.cs new file mode 100644 index 00000000000..18ae520fc2c --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSplitIncludeQueryTestBase.cs @@ -0,0 +1,138 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +// ReSharper disable FormatStringProblem +// ReSharper disable InconsistentNaming +// ReSharper disable ConvertToConstant.Local +// ReSharper disable AccessToDisposedClosure +namespace Microsoft.EntityFrameworkCore.Query +{ + public abstract class NorthwindSplitIncludeQueryTestBase : NorthwindIncludeQueryTestBase + where TFixture : NorthwindQueryFixtureBase, new() + { + private static readonly MethodInfo _asSplitIncludeMethodInfo + = typeof(RelationalQueryableExtensions) + .GetTypeInfo().GetDeclaredMethod(nameof(RelationalQueryableExtensions.AsSplitQuery)); + + protected NorthwindSplitIncludeQueryTestBase(TFixture fixture) + : base(fixture) + { + } + + public override async Task Include_closes_reader(bool async) + { + using var context = CreateContext(); + if (async) + { + Assert.NotNull(await context.Set().Include(c => c.Orders).AsNoTracking().FirstOrDefaultAsync()); + Assert.NotNull(await context.Set().AsNoTracking().ToListAsync()); + } + else + { + Assert.NotNull(context.Set().Include(c => c.Orders).AsNoTracking().FirstOrDefault()); + Assert.NotNull(context.Set().AsNoTracking().ToList()); + } + } + + public override async Task Include_collection_dependent_already_tracked(bool async) + { + using var context = CreateContext(); + var orders = context.Set().Where(o => o.CustomerID == "ALFKI").ToList(); + Assert.Equal(6, context.ChangeTracker.Entries().Count()); + Assert.True(orders.All(o => o.Customer == null)); + + var customer + = async + ? await context.Set() + .Include(c => c.Orders) + .AsNoTracking() + .SingleAsync(c => c.CustomerID == "ALFKI") + : context.Set() + .Include(c => c.Orders) + .AsNoTracking() + .Single(c => c.CustomerID == "ALFKI"); + + Assert.NotEqual(orders, customer.Orders, LegacyReferenceEqualityComparer.Instance); + Assert.Equal(6, customer.Orders.Count); + Assert.True(customer.Orders.All(e => ReferenceEquals(e.Customer, customer))); + + Assert.Equal(6, context.ChangeTracker.Entries().Count()); + Assert.True(orders.All(o => o.Customer == null)); + } + + public override async Task Include_collection_principal_already_tracked(bool async) + { + using var context = CreateContext(); + var customer1 = context.Set().Single(c => c.CustomerID == "ALFKI"); + Assert.Single(context.ChangeTracker.Entries()); + + var customer2 + = async + ? await context.Set() + .Include(c => c.Orders) + .AsNoTracking() + .SingleAsync(c => c.CustomerID == "ALFKI") + : context.Set() + .Include(c => c.Orders) + .AsNoTracking() + .Single(c => c.CustomerID == "ALFKI"); + + Assert.NotSame(customer1, customer2); + Assert.Equal(6, customer2.Orders.Count); + Assert.True(customer2.Orders.All(o => o.Customer != null)); + Assert.True(customer2.Orders.All(o => !ReferenceEquals(o.Customer, customer1))); + Assert.True(customer2.Orders.All(o => ReferenceEquals(o.Customer, customer2))); + + Assert.Single(context.ChangeTracker.Entries()); + } + + public override async Task Include_reference_dependent_already_tracked(bool async) + { + using var context = CreateContext(); + var customer = context.Set().Single(o => o.CustomerID == "ALFKI"); + Assert.Single(context.ChangeTracker.Entries()); + + var orders + = async + ? await context.Set().Include(o => o.Customer).AsNoTracking().Where(o => o.CustomerID == "ALFKI").ToListAsync() + : context.Set().Include(o => o.Customer).AsNoTracking().Where(o => o.CustomerID == "ALFKI").ToList(); + + Assert.Equal(6, orders.Count); + Assert.True(orders.All(o => !ReferenceEquals(o.Customer, customer))); + Assert.True(orders.All(o => o.Customer != null)); + Assert.Single(context.ChangeTracker.Entries()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(bool async) + { + return AssertQuery( + async, + ss => (from i in ss.Set().Include(o => o.Customer.Orders) + where i.OrderID < 10800 + select i) + .PerformIdentityResolution()); + } + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + { + serverQueryExpression = base.RewriteServerQueryExpression(serverQueryExpression); + + return Expression.Call( + _asSplitIncludeMethodInfo.MakeGenericMethod(serverQueryExpression.Type.TryGetSequenceType()), + serverQueryExpression); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs new file mode 100644 index 00000000000..3898327cd89 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs @@ -0,0 +1,1225 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class NorthwindSplitIncludeQuerySqlServerTest : NorthwindSplitIncludeQueryTestBase> + { + // ReSharper disable once UnusedParameter.Local + public NorthwindSplitIncludeQuerySqlServerTest(NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Include_list(bool async) + { + await base.Include_list(async); + + AssertSql( + @"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock], [t].[OrderID], [t].[ProductID], [t].[Discount], [t].[Quantity], [t].[UnitPrice], [t].[OrderID0], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate] +FROM [Products] AS [p] +LEFT JOIN ( + SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice], [o0].[OrderID] AS [OrderID0], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] + FROM [Order Details] AS [o] + INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +) AS [t] ON [p].[ProductID] = [t].[ProductID] +ORDER BY [p].[ProductID], [t].[OrderID], [t].[ProductID], [t].[OrderID0]"); + } + + public override async Task Include_reference(bool async) + { + await base.Include_reference(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID]"); + } + + public override async Task Include_when_result_operator(bool async) + { + await base.Include_when_result_operator(async); + + AssertSql( + @"SELECT CASE + WHEN EXISTS ( + SELECT 1 + FROM [Customers] AS [c]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END"); + } + + public override async Task Include_collection(bool async) + { + await base.Include_collection(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +LEFT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +ORDER BY [c].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_with_last(bool async) + { + await base.Include_collection_with_last(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(1) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CompanyName] DESC +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CompanyName] DESC, [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_skip_no_order_by(bool async) + { + await base.Include_collection_skip_no_order_by(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY (SELECT 1) + OFFSET @__p_0 ROWS +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_take_no_order_by(bool async) + { + await base.Include_collection_take_no_order_by(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_skip_take_no_order_by(bool async) + { + await base.Include_collection_skip_take_no_order_by(async); + + AssertSql( + @"@__p_0='10' +@__p_1='5' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY (SELECT 1) + OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_reference_and_collection(bool async) + { + await base.Include_reference_and_collection(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +LEFT JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +ORDER BY [o].[OrderID], [c].[CustomerID], [o0].[OrderID], [o0].[ProductID]"); + } + + public override async Task Include_references_multi_level(bool async) + { + await base.Include_references_multi_level(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID]"); + } + + public override async Task Include_multiple_references_multi_level(bool async) + { + await base.Include_multiple_references_multi_level(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] +INNER JOIN [Products] AS [p] ON [o].[ProductID] = [p].[ProductID]"); + } + + public override async Task Include_multiple_references_multi_level_reverse(bool async) + { + await base.Include_multiple_references_multi_level_reverse(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice], [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Order Details] AS [o] +INNER JOIN [Products] AS [p] ON [o].[ProductID] = [p].[ProductID] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID]"); + } + + public override async Task Include_references_and_collection_multi_level(bool async) + { + await base.Include_references_and_collection_multi_level(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] +LEFT JOIN [Orders] AS [o1] ON [c].[CustomerID] = [o1].[CustomerID] +ORDER BY [o].[OrderID], [o].[ProductID], [o0].[OrderID], [c].[CustomerID], [o1].[OrderID]"); + } + + public override async Task Include_multi_level_reference_and_collection_predicate(bool async) + { + await base.Include_multi_level_reference_and_collection_predicate(async); + + AssertSql( + @"SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [t].[CustomerID0], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT TOP(2) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID] AS [CustomerID0], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] + WHERE [o].[OrderID] = 10248 +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [t].[CustomerID0] = [o0].[CustomerID] +ORDER BY [t].[OrderID], [t].[CustomerID0], [o0].[OrderID]"); + } + + public override async Task Include_multi_level_collection_and_then_include_reference_predicate(bool async) + { + await base.Include_multi_level_collection_and_then_include_reference_predicate(async); + + AssertSql( + @"SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [t0].[OrderID], [t0].[ProductID], [t0].[Discount], [t0].[Quantity], [t0].[UnitPrice], [t0].[ProductID0], [t0].[Discontinued], [t0].[ProductName], [t0].[SupplierID], [t0].[UnitPrice0], [t0].[UnitsInStock] +FROM ( + SELECT TOP(2) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [o].[OrderID] = 10248 +) AS [t] +LEFT JOIN ( + SELECT [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice], [p].[ProductID] AS [ProductID0], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice] AS [UnitPrice0], [p].[UnitsInStock] + FROM [Order Details] AS [o0] + INNER JOIN [Products] AS [p] ON [o0].[ProductID] = [p].[ProductID] +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID], [t0].[ProductID], [t0].[ProductID0]"); + } + + public override async Task Include_collection_alias_generation(bool async) + { + await base.Include_collection_alias_generation(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] +FROM [Orders] AS [o] +LEFT JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +ORDER BY [o].[OrderID], [o0].[OrderID], [o0].[ProductID]"); + } + + public override async Task Include_collection_order_by_collection_column(bool async) + { + await base.Include_collection_order_by_collection_column(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT TOP(1) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], ( + SELECT TOP(1) [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderDate] DESC) AS [c] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'W%' + ORDER BY ( + SELECT TOP(1) [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderDate] DESC) DESC +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [t].[CustomerID] = [o0].[CustomerID] +ORDER BY [t].[c] DESC, [t].[CustomerID], [o0].[OrderID]"); + } + + public override async Task Include_collection_order_by_key(bool async) + { + await base.Include_collection_order_by_key(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +LEFT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +ORDER BY [c].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_order_by_non_key(bool async) + { + await base.Include_collection_order_by_non_key(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +LEFT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +ORDER BY [c].[PostalCode], [c].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_order_by_non_key_with_take(bool async) + { + await base.Include_collection_order_by_non_key_with_take(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[ContactTitle] +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[ContactTitle], [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_order_by_non_key_with_skip(bool async) + { + await base.Include_collection_order_by_non_key_with_skip(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[ContactTitle] + OFFSET @__p_0 ROWS +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[ContactTitle], [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_order_by_non_key_with_first_or_default(bool async) + { + await base.Include_collection_order_by_non_key_with_first_or_default(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(1) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CompanyName] DESC +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CompanyName] DESC, [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_order_by_subquery(bool async) + { + await base.Include_collection_order_by_subquery(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT TOP(1) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], ( + SELECT TOP(1) [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[EmployeeID]) AS [c] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] = N'ALFKI' + ORDER BY ( + SELECT TOP(1) [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[EmployeeID]) +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [t].[CustomerID] = [o0].[CustomerID] +ORDER BY [t].[c], [t].[CustomerID], [o0].[OrderID]"); + } + + public override async Task Include_collection_principal_already_tracked(bool async) + { + await base.Include_collection_principal_already_tracked(async); + + AssertSql( + @"SELECT TOP(2) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = N'ALFKI'", + // + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(2) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] = N'ALFKI' +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_with_filter(bool async) + { + await base.Include_collection_with_filter(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +LEFT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +WHERE [c].[CustomerID] = N'ALFKI' +ORDER BY [c].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_with_filter_reordered(bool async) + { + await base.Include_collection_with_filter_reordered(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +LEFT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +WHERE [c].[CustomerID] = N'ALFKI' +ORDER BY [c].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_then_include_collection(bool async) + { + await base.Include_collection_then_include_collection(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [t].[OrderID0], [t].[ProductID], [t].[Discount], [t].[Quantity], [t].[UnitPrice] +FROM [Customers] AS [c] +LEFT JOIN ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID] AS [OrderID0], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] + FROM [Orders] AS [o] + LEFT JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +) AS [t] ON [c].[CustomerID] = [t].[CustomerID] +ORDER BY [c].[CustomerID], [t].[OrderID], [t].[OrderID0], [t].[ProductID]"); + } + + public override async Task Include_collection_then_include_collection_then_include_reference(bool async) + { + await base.Include_collection_then_include_collection_then_include_reference(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[OrderID0], [t0].[ProductID], [t0].[Discount], [t0].[Quantity], [t0].[UnitPrice], [t0].[ProductID0], [t0].[Discontinued], [t0].[ProductName], [t0].[SupplierID], [t0].[UnitPrice0], [t0].[UnitsInStock] +FROM [Customers] AS [c] +LEFT JOIN ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [t].[OrderID] AS [OrderID0], [t].[ProductID], [t].[Discount], [t].[Quantity], [t].[UnitPrice], [t].[ProductID0], [t].[Discontinued], [t].[ProductName], [t].[SupplierID], [t].[UnitPrice0], [t].[UnitsInStock] + FROM [Orders] AS [o] + LEFT JOIN ( + SELECT [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice], [p].[ProductID] AS [ProductID0], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice] AS [UnitPrice0], [p].[UnitsInStock] + FROM [Order Details] AS [o0] + INNER JOIN [Products] AS [p] ON [o0].[ProductID] = [p].[ProductID] + ) AS [t] ON [o].[OrderID] = [t].[OrderID] +) AS [t0] ON [c].[CustomerID] = [t0].[CustomerID] +ORDER BY [c].[CustomerID], [t0].[OrderID], [t0].[OrderID0], [t0].[ProductID], [t0].[ProductID0]"); + } + + public override async Task Include_collection_when_projection(bool async) + { + await base.Include_collection_when_projection(async); + + AssertSql( + @"SELECT [c].[CustomerID] +FROM [Customers] AS [c]"); + } + + public override async Task Include_collection_with_join_clause_with_filter(bool async) + { + await base.Include_collection_with_join_clause_with_filter(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +INNER JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_collection_with_left_join_clause_with_filter(bool async) + { + await base.Include_collection_with_left_join_clause_with_filter(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +LEFT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [o].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_collection_with_cross_join_clause_with_filter(bool async) + { + await base.Include_collection_with_cross_join_clause_with_filter(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +CROSS JOIN ( + SELECT TOP(5) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[OrderID] +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [t].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_collection_with_cross_apply_with_filter(bool async) + { + await base.Include_collection_with_cross_apply_with_filter(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +CROSS APPLY ( + SELECT TOP(5) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID] AS [CustomerID0] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [c].[CustomerID] +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [t].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_collection_with_outer_apply_with_filter(bool async) + { + await base.Include_collection_with_outer_apply_with_filter(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT TOP(5) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID] AS [CustomerID0] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [c].[CustomerID] +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [t].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_collection_on_additional_from_clause_with_filter(bool async) + { + await base.Include_collection_on_additional_from_clause_with_filter(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [c].[CustomerID], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +CROSS JOIN ( + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + WHERE [c0].[CustomerID] = N'ALFKI' +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [c].[CustomerID], [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_on_additional_from_clause(bool async) + { + await base.Include_collection_on_additional_from_clause(async); + + AssertSql( + @"@__p_0='5' + +SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], [t].[CustomerID], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CustomerID] +) AS [t] +CROSS JOIN [Customers] AS [c0] +LEFT JOIN [Orders] AS [o] ON [c0].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [c0].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_duplicate_collection(bool async) + { + await base.Include_duplicate_collection(async); + + AssertSql( + @"@__p_0='2' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CustomerID] +) AS [t] +CROSS JOIN ( + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + ORDER BY [c0].[CustomerID] + OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY +) AS [t0] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [t0].[CustomerID] = [o0].[CustomerID] +ORDER BY [t].[CustomerID], [t0].[CustomerID], [o].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_duplicate_collection_result_operator(bool async) + { + await base.Include_duplicate_collection_result_operator(async); + + AssertSql( + @"@__p_1='1' +@__p_0='2' + +SELECT [t1].[CustomerID], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region], [t1].[CustomerID0], [t1].[Address0], [t1].[City0], [t1].[CompanyName0], [t1].[ContactName0], [t1].[ContactTitle0], [t1].[Country0], [t1].[Fax0], [t1].[Phone0], [t1].[PostalCode0], [t1].[Region0], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT TOP(@__p_1) [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [t0].[CustomerID] AS [CustomerID0], [t0].[Address] AS [Address0], [t0].[City] AS [City0], [t0].[CompanyName] AS [CompanyName0], [t0].[ContactName] AS [ContactName0], [t0].[ContactTitle] AS [ContactTitle0], [t0].[Country] AS [Country0], [t0].[Fax] AS [Fax0], [t0].[Phone] AS [Phone0], [t0].[PostalCode] AS [PostalCode0], [t0].[Region] AS [Region0] + FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CustomerID] + ) AS [t] + CROSS JOIN ( + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + ORDER BY [c0].[CustomerID] + OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY + ) AS [t0] + ORDER BY [t].[CustomerID] +) AS [t1] +LEFT JOIN [Orders] AS [o] ON [t1].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [t1].[CustomerID0] = [o0].[CustomerID] +ORDER BY [t1].[CustomerID], [t1].[CustomerID0], [o].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_collection_on_join_clause_with_order_by_and_filter(bool async) + { + await base.Include_collection_on_join_clause_with_order_by_and_filter(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Customers] AS [c] +INNER JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] +WHERE [c].[CustomerID] = N'ALFKI' +ORDER BY [c].[City], [c].[CustomerID], [o].[OrderID], [o0].[OrderID]"); + } + + public override async Task Include_collection_on_additional_from_clause2(bool async) + { + await base.Include_collection_on_additional_from_clause2(async); + + AssertSql( + @"@__p_0='5' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CustomerID] +) AS [t] +CROSS JOIN [Customers] AS [c0] +ORDER BY [t].[CustomerID]"); + } + + public override async Task Include_where_skip_take_projection(bool async) + { + await base.Include_where_skip_take_projection(async); + + AssertSql( + @"@__p_0='1' +@__p_1='2' + +SELECT [o0].[CustomerID] +FROM ( + SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] + FROM [Order Details] AS [o] + WHERE [o].[Quantity] = CAST(10 AS smallint) + ORDER BY [o].[OrderID], [o].[ProductID] + OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY +) AS [t] +INNER JOIN [Orders] AS [o0] ON [t].[OrderID] = [o0].[OrderID] +ORDER BY [t].[OrderID], [t].[ProductID]"); + } + + public override async Task Include_duplicate_collection_result_operator2(bool async) + { + await base.Include_duplicate_collection_result_operator2(async); + + AssertSql( + @"@__p_1='1' +@__p_0='2' + +SELECT [t1].[CustomerID], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region], [t1].[CustomerID0], [t1].[Address0], [t1].[City0], [t1].[CompanyName0], [t1].[ContactName0], [t1].[ContactTitle0], [t1].[Country0], [t1].[Fax0], [t1].[Phone0], [t1].[PostalCode0], [t1].[Region0], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(@__p_1) [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [t0].[CustomerID] AS [CustomerID0], [t0].[Address] AS [Address0], [t0].[City] AS [City0], [t0].[CompanyName] AS [CompanyName0], [t0].[ContactName] AS [ContactName0], [t0].[ContactTitle] AS [ContactTitle0], [t0].[Country] AS [Country0], [t0].[Fax] AS [Fax0], [t0].[Phone] AS [Phone0], [t0].[PostalCode] AS [PostalCode0], [t0].[Region] AS [Region0] + FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CustomerID] + ) AS [t] + CROSS JOIN ( + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] + FROM [Customers] AS [c0] + ORDER BY [c0].[CustomerID] + OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY + ) AS [t0] + ORDER BY [t].[CustomerID] +) AS [t1] +LEFT JOIN [Orders] AS [o] ON [t1].[CustomerID] = [o].[CustomerID] +ORDER BY [t1].[CustomerID], [t1].[CustomerID0], [o].[OrderID]"); + } + + public override async Task Include_multiple_references(bool async) + { + await base.Include_multiple_references(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +INNER JOIN [Products] AS [p] ON [o].[ProductID] = [p].[ProductID]"); + } + + public override async Task Include_reference_alias_generation(bool async) + { + await base.Include_reference_alias_generation(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM [Order Details] AS [o] +INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID]"); + } + + public override async Task Include_duplicate_reference(bool async) + { + await base.Include_duplicate_reference(async); + + AssertSql( + @"@__p_0='2' + +SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] +FROM ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[CustomerID], [o].[OrderID] +) AS [t] +CROSS JOIN ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] + FROM [Orders] AS [o0] + ORDER BY [o0].[CustomerID], [o0].[OrderID] + OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY +) AS [t0] +LEFT JOIN [Customers] AS [c] ON [t].[CustomerID] = [c].[CustomerID] +LEFT JOIN [Customers] AS [c0] ON [t0].[CustomerID] = [c0].[CustomerID] +ORDER BY [t].[CustomerID], [t].[OrderID]"); + } + + public override async Task Include_duplicate_reference2(bool async) + { + await base.Include_duplicate_reference2(async); + + AssertSql( + @"@__p_0='2' + +SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate] +FROM ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[OrderID] +) AS [t] +CROSS JOIN ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] + FROM [Orders] AS [o0] + ORDER BY [o0].[OrderID] + OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY +) AS [t0] +LEFT JOIN [Customers] AS [c] ON [t].[CustomerID] = [c].[CustomerID] +ORDER BY [t].[OrderID]"); + } + + public override async Task Include_duplicate_reference3(bool async) + { + await base.Include_duplicate_reference3(async); + + AssertSql( + @"@__p_0='2' + +SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[OrderID] +) AS [t] +CROSS JOIN ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] + FROM [Orders] AS [o0] + ORDER BY [o0].[OrderID] + OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY +) AS [t0] +LEFT JOIN [Customers] AS [c] ON [t0].[CustomerID] = [c].[CustomerID] +ORDER BY [t].[OrderID]"); + } + + public override async Task Include_reference_when_projection(bool async) + { + await base.Include_reference_when_projection(async); + + AssertSql( + @"SELECT [o].[CustomerID] +FROM [Orders] AS [o]"); + } + + public override async Task Include_reference_with_filter_reordered(bool async) + { + await base.Include_reference_with_filter_reordered(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +WHERE [o].[CustomerID] = N'ALFKI'"); + } + + public override async Task Include_reference_with_filter(bool async) + { + await base.Include_reference_with_filter(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +WHERE [o].[CustomerID] = N'ALFKI'"); + } + + public override async Task Include_collection_dependent_already_tracked(bool async) + { + await base.Include_collection_dependent_already_tracked(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Orders] AS [o] +WHERE [o].[CustomerID] = N'ALFKI'", + // + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(2) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] = N'ALFKI' +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_reference_dependent_already_tracked(bool async) + { + await base.Include_reference_dependent_already_tracked(async); + + AssertSql( + @"SELECT TOP(2) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = N'ALFKI'", + // + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +WHERE [o].[CustomerID] = N'ALFKI'"); + } + + public override async Task Include_with_complex_projection(bool async) + { + await base.Include_with_complex_projection(async); + + AssertSql( + @"SELECT [c].[CustomerID] AS [Id] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID]"); + } + + public override async Task Include_with_complex_projection_does_not_change_ordering_of_projection(bool async) + { + await base.Include_with_complex_projection_does_not_change_ordering_of_projection(async); + + AssertSql( + @"SELECT [c].[CustomerID] AS [Id], ( + SELECT COUNT(*) + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]) AS [TotalOrders] +FROM [Customers] AS [c] +WHERE ([c].[ContactTitle] = N'Owner') AND (( + SELECT COUNT(*) + FROM [Orders] AS [o0] + WHERE [c].[CustomerID] = [o0].[CustomerID]) > 2) +ORDER BY [c].[CustomerID]"); + } + + public override async Task Include_with_take(bool async) + { + await base.Include_with_take(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[ContactName] DESC +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[ContactName] DESC, [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_with_skip(bool async) + { + await base.Include_with_skip(async); + + AssertSql( + @"@__p_0='80' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[ContactName] + OFFSET @__p_0 ROWS +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[ContactName], [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_with_multiple_conditional_order_by(bool async) + { + await base.Include_collection_with_multiple_conditional_order_by(async); + + AssertSql( + @"@__p_0='5' + +SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [t].[CustomerID0], [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] +FROM ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID] AS [CustomerID0], CASE + WHEN [o].[OrderID] > 0 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c], CASE + WHEN [c].[CustomerID] IS NOT NULL THEN [c].[City] + ELSE N'' + END AS [c0] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] + ORDER BY CASE + WHEN [o].[OrderID] > 0 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END, CASE + WHEN [c].[CustomerID] IS NOT NULL THEN [c].[City] + ELSE N'' + END +) AS [t] +LEFT JOIN [Order Details] AS [o0] ON [t].[OrderID] = [o0].[OrderID] +ORDER BY [t].[c], [t].[c0], [t].[OrderID], [t].[CustomerID0], [o0].[OrderID], [o0].[ProductID]"); + } + + public override async Task Then_include_collection_order_by_collection_column(bool async) + { + await base.Then_include_collection_order_by_collection_column(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[OrderID0], [t0].[ProductID], [t0].[Discount], [t0].[Quantity], [t0].[UnitPrice] +FROM ( + SELECT TOP(1) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], ( + SELECT TOP(1) [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderDate] DESC) AS [c] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'W%' + ORDER BY ( + SELECT TOP(1) [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderDate] DESC) DESC +) AS [t] +LEFT JOIN ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [o1].[OrderID] AS [OrderID0], [o1].[ProductID], [o1].[Discount], [o1].[Quantity], [o1].[UnitPrice] + FROM [Orders] AS [o0] + LEFT JOIN [Order Details] AS [o1] ON [o0].[OrderID] = [o1].[OrderID] +) AS [t0] ON [t].[CustomerID] = [t0].[CustomerID] +ORDER BY [t].[c] DESC, [t].[CustomerID], [t0].[OrderID], [t0].[OrderID0], [t0].[ProductID]"); + } + + public override async Task Include_collection_with_conditional_order_by(bool async) + { + await base.Include_collection_with_conditional_order_by(async); + + AssertSql( + @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM [Customers] AS [c] +LEFT JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID] +ORDER BY CASE + WHEN [c].[CustomerID] LIKE N'S%' THEN 1 + ELSE 2 +END, [c].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_reference_distinct_is_server_evaluated(bool async) + { + await base.Include_reference_distinct_is_server_evaluated(async); + + AssertSql( + @"SELECT [t].[OrderID], [t].[CustomerID], [t].[EmployeeID], [t].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM ( + SELECT DISTINCT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + WHERE [o].[OrderID] < 10250 +) AS [t] +LEFT JOIN [Customers] AS [c] ON [t].[CustomerID] = [c].[CustomerID]"); + } + + public override async Task Include_collection_distinct_is_server_evaluated(bool async) + { + await base.Include_collection_distinct_is_server_evaluated(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT DISTINCT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'A%' +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_OrderBy_object(bool async) + { + await base.Include_collection_OrderBy_object(async); + + AssertSql( + @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] +FROM [Orders] AS [o] +LEFT JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +WHERE [o].[OrderID] < 10250 +ORDER BY [o].[OrderID], [o0].[OrderID], [o0].[ProductID]"); + } + + public override async Task Include_collection_OrderBy_empty_list_contains(bool async) + { + await base.Include_collection_OrderBy_empty_list_contains(async); + + AssertSql( + @"@__p_1='1' + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'A%' +ORDER BY (SELECT 1), [c].[CustomerID] +OFFSET @__p_1 ROWS", + // + @"@__p_1='1' + +SELECT [c.Orders].[OrderID], [c.Orders].[CustomerID], [c.Orders].[EmployeeID], [c.Orders].[OrderDate] +FROM [Orders] AS [c.Orders] +INNER JOIN ( + SELECT [c0].[CustomerID], CAST(0 AS bit) AS [c] + FROM [Customers] AS [c0] + WHERE [c0].[CustomerID] LIKE N'A%' + ORDER BY [c], [c0].[CustomerID] + OFFSET @__p_1 ROWS +) AS [t] ON [c.Orders].[CustomerID] = [t].[CustomerID] +ORDER BY [t].[c], [t].[CustomerID]"); + } + + public override async Task Include_collection_OrderBy_empty_list_does_not_contains(bool async) + { + await base.Include_collection_OrderBy_empty_list_does_not_contains(async); + + AssertSql( + @"@__p_1='1' + +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] LIKE N'A%' +ORDER BY (SELECT 1), [c].[CustomerID] +OFFSET @__p_1 ROWS", + // + @"@__p_1='1' + +SELECT [c.Orders].[OrderID], [c.Orders].[CustomerID], [c.Orders].[EmployeeID], [c.Orders].[OrderDate] +FROM [Orders] AS [c.Orders] +INNER JOIN ( + SELECT [c0].[CustomerID], CAST(1 AS bit) AS [c] + FROM [Customers] AS [c0] + WHERE [c0].[CustomerID] LIKE N'A%' + ORDER BY [c], [c0].[CustomerID] + OFFSET @__p_1 ROWS +) AS [t] ON [c.Orders].[CustomerID] = [t].[CustomerID] +ORDER BY [t].[c], [t].[CustomerID]"); + } + + public override async Task Include_collection_OrderBy_list_contains(bool async) + { + await base.Include_collection_OrderBy_list_contains(async); + + AssertSql( + @"@__p_1='1' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN [c].[CustomerID] IN (N'ALFKI') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'A%' + ORDER BY CASE + WHEN [c].[CustomerID] IN (N'ALFKI') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + OFFSET @__p_1 ROWS +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[c], [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_collection_OrderBy_list_does_not_contains(bool async) + { + await base.Include_collection_OrderBy_list_does_not_contains(async); + + AssertSql( + @"@__p_1='1' + +SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region], CASE + WHEN [c].[CustomerID] NOT IN (N'ALFKI') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [c] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'A%' + ORDER BY CASE + WHEN [c].[CustomerID] NOT IN (N'ALFKI') THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + OFFSET @__p_1 ROWS +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[c], [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Include_is_not_ignored_when_projection_contains_client_method_and_complex_expression(bool async) + { + await base.Include_is_not_ignored_when_projection_contains_client_method_and_complex_expression(async); + + AssertSql( + @"SELECT CASE + WHEN [e0].[EmployeeID] IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title], [e0].[EmployeeID], [e0].[City], [e0].[Country], [e0].[FirstName], [e0].[ReportsTo], [e0].[Title] +FROM [Employees] AS [e] +LEFT JOIN [Employees] AS [e0] ON [e].[ReportsTo] = [e0].[EmployeeID] +WHERE ([e].[EmployeeID] = 1) OR ([e].[EmployeeID] = 2) +ORDER BY [e].[EmployeeID]"); + } + + public override async Task Multi_level_includes_are_applied_with_skip(bool async) + { + await base.Multi_level_includes_are_applied_with_skip(async); + + AssertSql( + @"@__p_0='1' + +SELECT [t].[CustomerID], [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[OrderID0], [t0].[ProductID], [t0].[Discount], [t0].[Quantity], [t0].[UnitPrice] +FROM ( + SELECT [c].[CustomerID] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'A%' + ORDER BY [c].[CustomerID] + OFFSET @__p_0 ROWS FETCH NEXT 1 ROWS ONLY +) AS [t] +LEFT JOIN ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID] AS [OrderID0], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] + FROM [Orders] AS [o] + LEFT JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +) AS [t0] ON [t].[CustomerID] = [t0].[CustomerID] +ORDER BY [t].[CustomerID], [t0].[OrderID], [t0].[OrderID0], [t0].[ProductID]"); + } + + public override async Task Multi_level_includes_are_applied_with_take(bool async) + { + await base.Multi_level_includes_are_applied_with_take(async); + + AssertSql( + @"@__p_0='1' + +SELECT [t0].[CustomerID], [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[OrderID0], [t1].[ProductID], [t1].[Discount], [t1].[Quantity], [t1].[UnitPrice] +FROM ( + SELECT TOP(1) [t].[CustomerID] + FROM ( + SELECT TOP(@__p_0) [c].[CustomerID] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'A%' + ORDER BY [c].[CustomerID] + ) AS [t] + ORDER BY [t].[CustomerID] +) AS [t0] +LEFT JOIN ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID] AS [OrderID0], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] + FROM [Orders] AS [o] + LEFT JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +) AS [t1] ON [t0].[CustomerID] = [t1].[CustomerID] +ORDER BY [t0].[CustomerID], [t1].[OrderID], [t1].[OrderID0], [t1].[ProductID]"); + } + + public override async Task Multi_level_includes_are_applied_with_skip_take(bool async) + { + await base.Multi_level_includes_are_applied_with_skip_take(async); + + AssertSql( + @"@__p_0='1' + +SELECT [t0].[CustomerID], [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[OrderID0], [t1].[ProductID], [t1].[Discount], [t1].[Quantity], [t1].[UnitPrice] +FROM ( + SELECT TOP(1) [t].[CustomerID] + FROM ( + SELECT [c].[CustomerID] + FROM [Customers] AS [c] + WHERE [c].[CustomerID] LIKE N'A%' + ORDER BY [c].[CustomerID] + OFFSET @__p_0 ROWS FETCH NEXT @__p_0 ROWS ONLY + ) AS [t] + ORDER BY [t].[CustomerID] +) AS [t0] +LEFT JOIN ( + SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [o0].[OrderID] AS [OrderID0], [o0].[ProductID], [o0].[Discount], [o0].[Quantity], [o0].[UnitPrice] + FROM [Orders] AS [o] + LEFT JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] +) AS [t1] ON [t0].[CustomerID] = [t1].[CustomerID] +ORDER BY [t0].[CustomerID], [t1].[OrderID], [t1].[OrderID0], [t1].[ProductID]"); + } + + private void AssertSql(params string[] expected) + { } //=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSplitIncludeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSplitIncludeQuerySqliteTest.cs new file mode 100644 index 00000000000..30e5087cade --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSplitIncludeQuerySqliteTest.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class NorthwindSplitIncludeQuerySqliteTest : NorthwindSplitIncludeQueryTestBase> + { + public NorthwindSplitIncludeQuerySqliteTest(NorthwindQuerySqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //TestSqlLoggerFactory.CaptureOutput(testOutputHelper); + } + } +}