From f2d18386c2297fec0cf56f86d3f426612a19eee1 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Wed, 11 Aug 2021 17:50:50 -0700 Subject: [PATCH] Query: Add support for GroupBy patterns beyond aggregate - Allow expanding navigations after GroupBy operator applied before reducing it to non-grouping Fixes #22609 - Translate FirstOrDefault over grouping element Fixes #12088 - Add ability to select N element over grouping element Fixes #13805 Overall approach: A grouping element (the range variable you get after applying GroupBy operator) is of type `IGrouping` which implements `IEnumerable`. Hence we treat this enumerable as if it is queryable during nav expansion phase. During translation phase we inject ShapedQueryExpression in place of the grouping element which is being enumerated. What this allows us is to expand navigation just like any other query root and translate a subquery similar to other subqueries to facilitate reusing same code for the tasks. During translation phase in relational layer, since aggregate operation can be lifted into projection for SelectExpression containing SQL GROUP BY. This code path works in 2 ways, when translating we try to combine predicate/distinct into the aggregate operation (so in future when we support custom aggregate operators, we don't have to understand the shape of it to modify it later. When adding this scalar subquery to SelectExpression, we try to pattern match it to see if we can lift it. Further during lifting, we also lift any additional joins in the subquery (which implies there were some joins expanded on grouping element before aggregate) including the navigation expanded from owned navigations. A pending TODO is to de-dupe navigation expanded. It is not straight forward since aliases of table would have changed when previous was lifted. Given every enumerable grouping element act as query root, every time we replace it inside a lambda expression, we need to create a copy of the root. Navigation expansion and individual queryableMethodTranslatingEV does this. So each root act and translate independently from each other. Bug fixes: - Fix a bug in identifying single result in InMemory to convert it to enumerable - Null out _groupingParameter in InMemoryQueryExpression once the projection to reduce it has been applied - Throw better error message when translating Min/Max over an entity type for InMemory --- .../Internal/EntityProjectionExpression.cs | 25 + ...yExpressionTranslatingExpressionVisitor.cs | 294 +-------- .../InMemoryGroupByShaperExpression.cs | 50 -- .../InMemoryQueryExpression.Helper.cs | 59 ++ .../Query/Internal/InMemoryQueryExpression.cs | 58 +- ...yableMethodTranslatingExpressionVisitor.cs | 21 +- ...yableMethodTranslatingExpressionVisitor.cs | 271 ++++++-- ...lationalSqlTranslatingExpressionVisitor.cs | 232 ------- .../SqlExpressions/SelectExpression.Helper.cs | 134 +++- .../Query/SqlExpressions/SelectExpression.cs | 108 +++- src/EFCore/Query/GroupByShaperExpression.cs | 30 +- ...ingExpressionVisitor.ExpressionVisitors.cs | 105 ++++ ...nExpandingExpressionVisitor.Expressions.cs | 65 +- .../NavigationExpandingExpressionVisitor.cs | 284 +++++++-- src/EFCore/Query/ProjectionMember.cs | 2 + .../TestUtilities/TestSqlLoggerFactory.cs | 4 +- ...plexNavigationsCollectionsQueryTestBase.cs | 6 +- .../Query/ComplexNavigationsQueryTestBase.cs | 4 +- .../Query/GearsOfWarQueryTestBase.cs | 4 +- .../Query/NorthwindGroupByQueryTestBase.cs | 109 ++-- .../Query/NorthwindIncludeQueryTestBase.cs | 50 +- .../NorthwindSetOperationsQueryTestBase.cs | 389 ++++++++++-- .../ProceduralQueryExpressionGenerator.cs | 4 - ...avigationsCollectionsQuerySqlServerTest.cs | 72 +++ ...tionsCollectionsSplitQuerySqlServerTest.cs | 108 +++- .../ComplexNavigationsQuerySqlServerTest.cs | 49 +- ...NavigationsSharedTypeQuerySqlServerTest.cs | 8 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 59 +- .../NorthwindGroupByQuerySqlServerTest.cs | 242 ++++---- .../NorthwindIncludeQuerySqlServerTest.cs | 259 ++++++++ ...orthwindSetOperationsQuerySqlServerTest.cs | 579 +++++++++++++++--- ...NorthwindSplitIncludeQuerySqlServerTest.cs | 357 +++++++++++ .../Query/OwnedQuerySqlServerTest.cs | 8 +- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 65 +- .../TableSplittingSqlServerTest.cs | 18 +- .../Query/NorthwindGroupByQuerySqliteTest.cs | 30 + 36 files changed, 3036 insertions(+), 1126 deletions(-) delete mode 100644 src/EFCore.InMemory/Query/Internal/InMemoryGroupByShaperExpression.cs diff --git a/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs b/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs index c96c0f73d89..6d73fe6dc30 100644 --- a/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs @@ -148,6 +148,31 @@ public virtual void AddNavigationBinding(INavigation navigation, EntityShaperExp : null; } + /// + /// 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 EntityProjectionExpression Clone() + { + var readExpressionMap = new Dictionary(); + foreach (var kvp in _readExpressionMap) + { + readExpressionMap.Add(kvp.Key, kvp.Value); + } + var entityProjectionExpression = new EntityProjectionExpression(EntityType, readExpressionMap); + foreach (var kvp in _navigationExpressionsCache) + { + entityProjectionExpression._navigationExpressionsCache[kvp.Key] = new EntityShaperExpression( + kvp.Value.EntityType, + ((EntityProjectionExpression)kvp.Value.ValueBufferExpression).Clone(), + kvp.Value.IsNullable); + } + + return entityProjectionExpression; + } + /// /// 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 diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 031d79195f8..6faa1a40e9c 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -30,7 +30,7 @@ namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal /// public class InMemoryExpressionTranslatingExpressionVisitor : ExpressionVisitor { - private const string _runtimeParameterPrefix = QueryCompilationContext.QueryParameterPrefix + "entity_equality_"; + private const string RuntimeParameterPrefix = QueryCompilationContext.QueryParameterPrefix + "entity_equality_"; private static readonly MemberInfo _valueBufferIsEmpty = typeof(ValueBuffer).GetMember(nameof(ValueBuffer.IsEmpty))[0]; @@ -280,12 +280,6 @@ protected override Expression VisitExtension(Expression extensionExpression) when projectionBindingExpression.ProjectionMember != null: return ((InMemoryQueryExpression)projectionBindingExpression.QueryExpression).GetProjection(projectionBindingExpression); - case InMemoryGroupByShaperExpression inMemoryGroupByShaperExpression: - return new GroupingElementExpression( - inMemoryGroupByShaperExpression.GroupingParameter, - inMemoryGroupByShaperExpression.ElementSelector, - inMemoryGroupByShaperExpression.ValueBufferParameter); - default: return QueryCompilationContext.NotTranslatedExpression; } @@ -461,246 +455,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp ?? QueryCompilationContext.NotTranslatedExpression; } - // GroupBy Aggregate case - if (methodCallExpression.Object == null - && methodCallExpression.Method.DeclaringType == typeof(Enumerable) - && methodCallExpression.Arguments.Count > 0) - { - if (methodCallExpression.Arguments[0].Type.TryGetElementType(typeof(IQueryable<>)) == null - && Visit(methodCallExpression.Arguments[0]) is GroupingElementExpression groupingElementExpression) - { - Expression? result = null; - switch (methodCallExpression.Method.Name) - { - case nameof(Enumerable.Average): - { - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - var expression = ApplySelect(groupingElementExpression); - - result = expression == null - ? null - : Expression.Call( - EnumerableMethods.GetAverageWithoutSelector(expression.Type.GetSequenceType()), expression); - break; - } - - case nameof(Enumerable.Count): - { - if (methodCallExpression.Arguments.Count == 2) - { - var temporaryGroupingElementExpression = ApplyPredicate( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - - if (temporaryGroupingElementExpression == null) - { - result = null; - break; - } - - groupingElementExpression = temporaryGroupingElementExpression; - } - - var expression = ApplySelect(groupingElementExpression); - - result = expression == null - ? null - : Expression.Call( - EnumerableMethods.CountWithoutPredicate.MakeGenericMethod(expression.Type.GetSequenceType()), - expression); - break; - } - - case nameof(Enumerable.Distinct): - result = groupingElementExpression.Selector is EntityShaperExpression - ? groupingElementExpression - : groupingElementExpression.IsDistinct - ? null - : groupingElementExpression.ApplyDistinct(); - break; - - case nameof(Enumerable.LongCount): - { - if (methodCallExpression.Arguments.Count == 2) - { - var temporaryGroupingElementExpression = ApplyPredicate( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - - if (temporaryGroupingElementExpression == null) - { - result = null; - break; - } - - groupingElementExpression = temporaryGroupingElementExpression; - } - - var expression = ApplySelect(groupingElementExpression); - - result = expression == null - ? null - : Expression.Call( - EnumerableMethods.LongCountWithoutPredicate.MakeGenericMethod(expression.Type.GetSequenceType()), - expression); - break; - } - - case nameof(Enumerable.Max): - { - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - var expression = ApplySelect(groupingElementExpression); - if (expression == null - || expression is ParameterExpression) - { - result = null; - } - else - { - var type = expression.Type.GetSequenceType(); - var aggregateMethod = EnumerableMethods.GetMaxWithoutSelector(type); - if (aggregateMethod.IsGenericMethod) - { - aggregateMethod = aggregateMethod.MakeGenericMethod(type); - } - - result = Expression.Call(aggregateMethod, expression); - } - - break; - } - - case nameof(Enumerable.Min): - { - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - var expression = ApplySelect(groupingElementExpression); - if (expression == null - || expression is ParameterExpression) - { - result = null; - } - else - { - var type = expression.Type.GetSequenceType(); - var aggregateMethod = EnumerableMethods.GetMinWithoutSelector(type); - if (aggregateMethod.IsGenericMethod) - { - aggregateMethod = aggregateMethod.MakeGenericMethod(type); - } - - result = Expression.Call(aggregateMethod, expression); - } - - break; - } - - case nameof(Enumerable.Select): - result = ApplySelector(groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - break; - - case nameof(Enumerable.Sum): - { - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - var expression = ApplySelect(groupingElementExpression); - - result = expression == null - ? null - : Expression.Call( - EnumerableMethods.GetSumWithoutSelector(expression.Type.GetSequenceType()), expression); - break; - } - - case nameof(Enumerable.Where): - result = ApplyPredicate(groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - break; - - default: - result = null; - break; - } - - return result ?? throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); - - GroupingElementExpression? ApplyPredicate(GroupingElementExpression groupingElement, LambdaExpression lambdaExpression) - { - var predicate = TranslateInternal(RemapLambda(groupingElement, lambdaExpression)); - - if (predicate == null) - { - return null; - } - - if (predicate.Type != typeof(bool)) - { - predicate = Expression.Equal(predicate, Expression.Constant(true, typeof(bool?))); - } - - return groupingElement.UpdateSource( - Expression.Call( - EnumerableMethods.Where.MakeGenericMethod(typeof(ValueBuffer)), - groupingElement.Source, - Expression.Lambda(predicate, groupingElement.ValueBufferParameter))); - } - - Expression? ApplySelect(GroupingElementExpression groupingElement) - { - var selector = TranslateInternal(groupingElement.Selector); - - if (selector == null) - { - return groupingElement.Selector is EntityShaperExpression - ? groupingElement.Source - : null; - } - - var result = Expression.Call( - EnumerableMethods.Select.MakeGenericMethod(typeof(ValueBuffer), selector.Type), - groupingElement.Source, - Expression.Lambda(selector, groupingElement.ValueBufferParameter)); - - if (groupingElement.IsDistinct) - { - result = Expression.Call( - EnumerableMethods.Distinct.MakeGenericMethod(selector.Type), - result); - } - - return result; - } - - static GroupingElementExpression ApplySelector( - GroupingElementExpression groupingElement, - LambdaExpression lambdaExpression) - { - var selector = RemapLambda(groupingElement, lambdaExpression); - - return groupingElement.ApplySelector(selector); - } - - static Expression RemapLambda(GroupingElementExpression groupingElement, LambdaExpression lambdaExpression) - => ReplacingExpressionVisitor.Replace( - lambdaExpression.Parameters[0], groupingElement.Selector, lambdaExpression.Body); - } - } - // Subquery case var subqueryTranslation = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(methodCallExpression); if (subqueryTranslation != null) @@ -1346,7 +1100,7 @@ when methodCallExpression.Method.IsGenericMethod ); var newParameterName = - $"{_runtimeParameterPrefix}" + $"{RuntimeParameterPrefix}" + $"{parameterName[QueryCompilationContext.QueryParameterPrefix.Length..]}_{property.Name}"; rewrittenSource = _queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); @@ -1477,7 +1231,7 @@ when methodCallExpression.Method.IsGenericMethod QueryCompilationContext.QueryContextParameter); var newParameterName = - $"{_runtimeParameterPrefix}" + $"{RuntimeParameterPrefix}" + $"{parameterName[QueryCompilationContext.QueryParameterPrefix.Length..]}_{property.Name}"; return _queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); @@ -1747,47 +1501,5 @@ public Expression Convert(Type type) : new EntityReferenceExpression(this, derivedEntityType); } } - - private sealed class GroupingElementExpression : Expression - { - public GroupingElementExpression(Expression source, Expression selector, ParameterExpression valueBufferParameter) - { - Source = source; - ValueBufferParameter = valueBufferParameter; - Selector = selector; - } - - public Expression Source { get; private set; } - public bool IsDistinct { get; private set; } - public Expression Selector { get; private set; } - public ParameterExpression ValueBufferParameter { get; } - - public GroupingElementExpression ApplyDistinct() - { - IsDistinct = true; - - return this; - } - - public GroupingElementExpression ApplySelector(Expression expression) - { - Selector = expression; - - return this; - } - - public GroupingElementExpression UpdateSource(Expression source) - { - Source = source; - - return this; - } - - public override Type Type - => typeof(IEnumerable<>).MakeGenericType(Selector.Type); - - public override ExpressionType NodeType - => ExpressionType.Extension; - } } } diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryGroupByShaperExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryGroupByShaperExpression.cs deleted file mode 100644 index 0e7c7697ebc..00000000000 --- a/src/EFCore.InMemory/Query/Internal/InMemoryGroupByShaperExpression.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Query; - -namespace Microsoft.EntityFrameworkCore.InMemory.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 InMemoryGroupByShaperExpression : GroupByShaperExpression - { - /// - /// 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 InMemoryGroupByShaperExpression( - Expression keySelector, - Expression elementSelector, - ParameterExpression groupingParameter, - ParameterExpression valueBufferParameter) - : base(keySelector, elementSelector) - { - GroupingParameter = groupingParameter; - ValueBufferParameter = valueBufferParameter; - } - - /// - /// 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 ParameterExpression GroupingParameter { 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 ParameterExpression ValueBufferParameter { get; } - } -} diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs index 6e152c9060b..cd59c853a86 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs @@ -173,5 +173,64 @@ protected override Expression VisitExtension(Expression extensionExpression) : base.VisitExtension(extensionExpression); } } + + private sealed class QueryExpressionReplacingExpressionVisitor : ExpressionVisitor + { + private readonly Expression _oldQuery; + private readonly Expression _newQuery; + + public QueryExpressionReplacingExpressionVisitor(Expression oldQuery, Expression newQuery) + { + _oldQuery = oldQuery; + _newQuery = newQuery; + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + return expression is ProjectionBindingExpression projectionBindingExpression + && ReferenceEquals(projectionBindingExpression.QueryExpression, _oldQuery) + ? projectionBindingExpression.ProjectionMember != null + ? new ProjectionBindingExpression( + _newQuery, projectionBindingExpression.ProjectionMember!, projectionBindingExpression.Type) + : new ProjectionBindingExpression( + _newQuery, projectionBindingExpression.Index!.Value, projectionBindingExpression.Type) + : base.Visit(expression); + } + } + + private sealed class CloningExpressionVisitor : ExpressionVisitor + { + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + if (expression is InMemoryQueryExpression inMemoryQueryExpression) + { + var clonedInMemoryQueryExpression = new InMemoryQueryExpression( + inMemoryQueryExpression.ServerQueryExpression, inMemoryQueryExpression._valueBufferParameter) + { + _groupingParameter = inMemoryQueryExpression._groupingParameter, + _singleResultMethodInfo = inMemoryQueryExpression._singleResultMethodInfo, + _scalarServerQuery = inMemoryQueryExpression._scalarServerQuery + }; + + clonedInMemoryQueryExpression._clientProjections.AddRange(inMemoryQueryExpression._clientProjections.Select(e => Visit(e))); + clonedInMemoryQueryExpression._projectionMappingExpressions.AddRange(inMemoryQueryExpression._projectionMappingExpressions); + foreach (var item in inMemoryQueryExpression._projectionMapping) + { + clonedInMemoryQueryExpression._projectionMapping[item.Key] = Visit(item.Value); + } + + return clonedInMemoryQueryExpression; + } + + if (expression is EntityProjectionExpression entityProjectionExpression) + { + return entityProjectionExpression.Clone(); + } + + return base.Visit(expression); + } + } } } diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs index 64ed98a783a..b693d555579 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs @@ -41,10 +41,20 @@ public partial class InMemoryQueryExpression : Expression, IPrintableExpression private MethodInfo? _singleResultMethodInfo; private bool _scalarServerQuery; + private CloningExpressionVisitor? _cloningExpressionVisitor; + private Dictionary _projectionMapping = new(); private readonly List _clientProjections = new(); private readonly List _projectionMappingExpressions = new(); + private InMemoryQueryExpression( + Expression serverQueryExpression, + ParameterExpression valueBufferParameter) + { + ServerQueryExpression = serverQueryExpression; + _valueBufferParameter = valueBufferParameter; + } + /// /// 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 @@ -333,15 +343,17 @@ public virtual void ApplyProjection() ServerQueryExpression, selectorLambda); + _groupingParameter = null; + if (_singleResultMethodInfo != null) { ServerQueryExpression = Call( _singleResultMethodInfo.MakeGenericMethod(CurrentParameter.Type), ServerQueryExpression); - _singleResultMethodInfo = null; - ConvertToEnumerable(); + + _singleResultMethodInfo = null; } } @@ -540,7 +552,7 @@ public virtual void ApplyDistinct() /// 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 InMemoryGroupByShaperExpression ApplyGrouping( + public virtual GroupByShaperExpression ApplyGrouping( Expression groupingKey, Expression shaperExpression, bool defaultElementSelector) @@ -583,11 +595,15 @@ public virtual void ApplyDistinct() keySelector, selector); - return new InMemoryGroupByShaperExpression( + var clonedInMemoryQueryExpression = Clone(); + clonedInMemoryQueryExpression.UpdateServerQueryExpression(_groupingParameter); + clonedInMemoryQueryExpression._groupingParameter = null; + + return new GroupByShaperExpression( groupingKey, - shaperExpression, - _groupingParameter, - _valueBufferParameter); + new ShapedQueryExpression( + clonedInMemoryQueryExpression, + new QueryExpressionReplacingExpressionVisitor(this, clonedInMemoryQueryExpression).Visit(shaperExpression))); } /// @@ -711,6 +727,22 @@ public virtual void ApplyDistinct() return entityShaper; } + /// + /// 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 ShapedQueryExpression Clone(Expression shaperExpression) + { + var clonedInMemoryQueryExpression = Clone(); + + return new ShapedQueryExpression( + clonedInMemoryQueryExpression, + new QueryExpressionReplacingExpressionVisitor(this, clonedInMemoryQueryExpression).Visit(shaperExpression)); + + } + /// /// 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 @@ -811,6 +843,16 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) } } + private InMemoryQueryExpression Clone() + { + if (_cloningExpressionVisitor == null) + { + _cloningExpressionVisitor = new(); + } + + return (InMemoryQueryExpression)_cloningExpressionVisitor.Visit(this); + } + private Expression GetGroupingKey(Expression key, List groupingExpressions, Expression groupingKeyAccessExpression) { switch (key) @@ -1061,7 +1103,7 @@ static Expression MakeNullable(Expression expression, bool nullable) private void ConvertToEnumerable() { - if (ServerQueryExpression.Type.TryGetSequenceType() == null) + if (_scalarServerQuery || _singleResultMethodInfo != null) { if (ServerQueryExpression.Type != typeof(ValueBuffer)) { diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index c3de097e64b..0e9bcfeef79 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -70,6 +70,24 @@ public class InMemoryQueryableMethodTranslatingExpressionVisitor : QueryableMeth protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() => new InMemoryQueryableMethodTranslatingExpressionVisitor(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. + /// + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is GroupByShaperExpression groupByShaperExpression) + { + var shapedQueryExpression = groupByShaperExpression.GroupingEnumerable; + return ((InMemoryQueryExpression)shapedQueryExpression.QueryExpression) + .Clone(shapedQueryExpression.ShaperExpression); + } + + return base.VisitExtension(extensionExpression); + } + /// /// 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 @@ -1513,7 +1531,8 @@ private ShapedQueryExpression TranslateTwoParameterSelector(ShapedQueryExpressio inMemoryQueryExpression.CurrentParameter) : TranslateLambdaExpression(source, selector, preserveType: true); - if (selector == null) + if (selector == null + || selector.Body is EntityProjectionExpression) { return null; } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 5a2b35ae572..3bdaec70a74 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -26,6 +27,7 @@ public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMe private readonly QueryCompilationContext _queryCompilationContext; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly bool _subquery; + private SqlExpression? _groupingElementCorrelationalPredicate; /// /// Creates a new instance of the class. @@ -142,11 +144,35 @@ protected override Expression VisitExtension(Expression extensionExpression) new FromSqlQueryRootExpression( queryRootExpression.EntityType, sqlQuery.Sql, Expression.Constant(Array.Empty(), typeof(object[])))); + case GroupByShaperExpression groupByShaperExpression: + var shapedQueryExpression = groupByShaperExpression.GroupingEnumerable; + var clonedSelectExpression = ((SelectExpression)shapedQueryExpression.QueryExpression).Clone(); + _groupingElementCorrelationalPredicate = clonedSelectExpression.Predicate; + return new ShapedQueryExpression( + clonedSelectExpression, + new QueryExpressionReplacingExpressionVisitor( + shapedQueryExpression.QueryExpression, clonedSelectExpression).Visit(shapedQueryExpression.ShaperExpression)); + default: return base.VisitExtension(extensionExpression); } } + /// + public override ShapedQueryExpression? TranslateSubquery(Expression expression) + { + Check.NotNull(expression, nameof(expression)); + + var subqueryVisitor = (RelationalQueryableMethodTranslatingExpressionVisitor)CreateSubqueryVisitor(); + var translation = subqueryVisitor.Visit(expression) as ShapedQueryExpression; + if (translation == null && subqueryVisitor.TranslationErrorDetails != null) + { + AddTranslationErrorDetails(subqueryVisitor.TranslationErrorDetails); + } + + return translation; + } + /// protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() => new RelationalQueryableMethodTranslatingExpressionVisitor(this); @@ -320,36 +346,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent { Check.NotNull(source, nameof(source)); - var selectExpression = (SelectExpression)source.QueryExpression; - selectExpression.PrepareForAggregate(); - - if (predicate != null) - { - var translatedSource = TranslateWhere(source, predicate); - if (translatedSource == null) - { - return null; - } - source = translatedSource; - } - - HandleGroupByForAggregate(selectExpression, eraseProjection: true); - - var translation = _sqlTranslator.TranslateCount(_sqlExpressionFactory.Fragment("*")); - if (translation == null) - { - return null; - } - - var projectionMapping = new Dictionary { { new ProjectionMember(), translation } }; - - selectExpression.ClearOrdering(); - selectExpression.ReplaceProjection(projectionMapping); - - return source.UpdateShaperExpression( - Expression.Convert( - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(int?)), - typeof(int))); + return TranslateAggregateWithPredicate(source, predicate, e => _sqlTranslator.TranslateCount(e), typeof(int)); } /// @@ -464,7 +461,6 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent } var remappedKeySelector = RemapLambdaBody(source, keySelector); - var translatedKey = TranslateGroupingKey(remappedKeySelector); if (translatedKey != null) { @@ -473,8 +469,28 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent source = TranslateSelect(source, elementSelector); } - translatedKey = selectExpression.ApplyGrouping(translatedKey); - var groupByShaper = new GroupByShaperExpression(translatedKey, source.ShaperExpression); + if (translatedKey is NewExpression newExpression + && newExpression.Arguments.Count == 0) + { + selectExpression.ApplyGrouping(_sqlExpressionFactory.ApplyDefaultTypeMapping(_sqlExpressionFactory.Constant(1))); + } + else + { + translatedKey = selectExpression.ApplyGrouping(translatedKey); + } + var clonedSelectExpression = selectExpression.Clone(); + // If the grouping key is empty then there may not be any group by terms. + var correlationPredicate = selectExpression.GroupBy.Zip(clonedSelectExpression.GroupBy) + .Select(e => _sqlExpressionFactory.Equal(e.First, e.Second)) + .Aggregate((l, r) => _sqlExpressionFactory.AndAlso(l, r)); + clonedSelectExpression.ClearGroupBy(); + clonedSelectExpression.ApplyPredicate(correlationPredicate); + + var groupByShaper = new GroupByShaperExpression( + translatedKey, + new ShapedQueryExpression( + clonedSelectExpression, + new QueryExpressionReplacingExpressionVisitor(selectExpression, clonedSelectExpression).Visit(source.ShaperExpression))); if (resultSelector == null) { @@ -720,36 +736,7 @@ private SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerK { Check.NotNull(source, nameof(source)); - var selectExpression = (SelectExpression)source.QueryExpression; - selectExpression.PrepareForAggregate(); - - if (predicate != null) - { - var translatedSource = TranslateWhere(source, predicate); - if (translatedSource == null) - { - return null; - } - source = translatedSource; - } - - HandleGroupByForAggregate(selectExpression, eraseProjection: true); - - var translation = _sqlTranslator.TranslateLongCount(_sqlExpressionFactory.Fragment("*")); - if (translation == null) - { - return null; - } - - var projectionMapping = new Dictionary { { new ProjectionMember(), translation } }; - - selectExpression.ClearOrdering(); - selectExpression.ReplaceProjection(projectionMapping); - - return source.UpdateShaperExpression( - Expression.Convert( - new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), typeof(long?)), - typeof(long))); + return TranslateAggregateWithPredicate(source, predicate, e => _sqlTranslator.TranslateLongCount(e), typeof(long)); } /// @@ -1241,7 +1228,7 @@ protected override Expression VisitExtension(Expression extensionExpression) private Expression? TryExpand(Expression? source, MemberIdentity member) { source = source.UnwrapTypeConversion(out var convertedType); - if (!(source is EntityShaperExpression entityShaperExpression)) + if (source is not EntityShaperExpression entityShaperExpression) { return null; } @@ -1495,6 +1482,75 @@ private Expression MatchShaperNullabilityForSetOperation(Expression shaper1, Exp } } + private ShapedQueryExpression? TranslateAggregateWithPredicate( + ShapedQueryExpression source, + LambdaExpression? predicate, + Func aggregateTranslator, + Type resultType) + { + var selectExpression = (SelectExpression)source.QueryExpression; + if (_groupingElementCorrelationalPredicate == null) + { + selectExpression.PrepareForAggregate(); + } + + if (predicate != null) + { + var translatedSource = TranslateWhere(source, predicate); + if (translatedSource == null) + { + return null; + } + source = translatedSource; + } + + SqlExpression sqlExpression = _sqlExpressionFactory.Fragment("*"); + + if (_groupingElementCorrelationalPredicate != null) + { + if (selectExpression.IsDistinct) + { + var shaperExpression = source.ShaperExpression; + if (shaperExpression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert) + { + shaperExpression = unaryExpression.Operand; + } + + if (shaperExpression is ProjectionBindingExpression projectionBindingExpression) + { + sqlExpression = (SqlExpression)selectExpression.GetProjection(projectionBindingExpression); + } + else + { + return null; + } + } + + sqlExpression = CombineGroupByAggregateTerms(selectExpression, sqlExpression); + } + else + { + HandleGroupByForAggregate(selectExpression, eraseProjection: true); + } + + var translation = aggregateTranslator(sqlExpression); + if (translation == null) + { + return null; + } + + var projectionMapping = new Dictionary { { new ProjectionMember(), translation } }; + + selectExpression.ClearOrdering(); + selectExpression.ReplaceProjection(projectionMapping); + + return source.UpdateShaperExpression( + Expression.Convert( + new ProjectionBindingExpression(source.QueryExpression, new ProjectionMember(), resultType.MakeNullable()), + resultType)); + } + private ShapedQueryExpression? TranslateAggregateWithSelector( ShapedQueryExpression source, LambdaExpression? selector, @@ -1503,8 +1559,11 @@ private Expression MatchShaperNullabilityForSetOperation(Expression shaper1, Exp Type resultType) { var selectExpression = (SelectExpression)source.QueryExpression; - selectExpression.PrepareForAggregate(); - HandleGroupByForAggregate(selectExpression); + if (_groupingElementCorrelationalPredicate == null) + { + selectExpression.PrepareForAggregate(); + HandleGroupByForAggregate(selectExpression); + } SqlExpression translatedSelector; if (selector == null @@ -1539,6 +1598,11 @@ private Expression MatchShaperNullabilityForSetOperation(Expression shaper1, Exp } } + if (_groupingElementCorrelationalPredicate != null) + { + translatedSelector = CombineGroupByAggregateTerms(selectExpression, translatedSelector); + } + var projection = aggregateTranslator(translatedSelector); if (projection == null) { @@ -1595,5 +1659,80 @@ private Expression MatchShaperNullabilityForSetOperation(Expression shaper1, Exp return source.UpdateShaperExpression(shaper); } + + private SqlExpression CombineGroupByAggregateTerms(SelectExpression selectExpression, SqlExpression selector) + { + if (selectExpression.Predicate != null + && !selectExpression.Predicate.Equals(_groupingElementCorrelationalPredicate)) + { + if (selector is SqlFragmentExpression sqlFragmentExpression + && sqlFragmentExpression.Sql == "*") + { + selector = _sqlExpressionFactory.Constant(1); + } + + var correlationTerms = new List(); + var predicateTerms = new List(); + PopulatePredicateTerms(_groupingElementCorrelationalPredicate!, correlationTerms); + PopulatePredicateTerms(selectExpression.Predicate, predicateTerms); + var predicate = predicateTerms.Skip(correlationTerms.Count) + .Aggregate((l, r) => _sqlExpressionFactory.AndAlso(l, r)); + selector = _sqlExpressionFactory.Case( + new List { new CaseWhenClause(predicate, selector) }, + elseResult: null); + } + + if (selectExpression.IsDistinct) + { + if (selector is SqlFragmentExpression sqlFragmentExpression + && sqlFragmentExpression.Sql == "*") + { + selector = _sqlExpressionFactory.Constant(1); + } + + selector = new DistinctExpression(selector); + } + + return selector; + + static void PopulatePredicateTerms(SqlExpression predicate, List terms) + { + if (predicate is SqlBinaryExpression sqlBinaryExpression + && sqlBinaryExpression.OperatorType == ExpressionType.AndAlso) + { + PopulatePredicateTerms(sqlBinaryExpression.Left, terms); + PopulatePredicateTerms(sqlBinaryExpression.Right, terms); + } + else + { + terms.Add(predicate); + } + } + } + + private sealed class QueryExpressionReplacingExpressionVisitor : ExpressionVisitor + { + private readonly Expression _oldQuery; + private readonly Expression _newQuery; + + public QueryExpressionReplacingExpressionVisitor(Expression oldQuery, Expression newQuery) + { + _oldQuery = oldQuery; + _newQuery = newQuery; + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + return expression is ProjectionBindingExpression projectionBindingExpression + && ReferenceEquals(projectionBindingExpression.QueryExpression, _oldQuery) + ? projectionBindingExpression.ProjectionMember != null + ? new ProjectionBindingExpression( + _newQuery, projectionBindingExpression.ProjectionMember!, projectionBindingExpression.Type) + : new ProjectionBindingExpression( + _newQuery, projectionBindingExpression.Index!.Value, projectionBindingExpression.Type) + : base.Visit(expression); + } + } } } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index c5cc1024fab..926f64f5877 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -413,9 +413,6 @@ protected override Expression VisitExtension(Expression extensionExpression) return ((SelectExpression)projectionBindingExpression.QueryExpression) .GetProjection(projectionBindingExpression); - case GroupByShaperExpression groupByShaperExpression: - return new GroupingElementExpression(groupByShaperExpression.ElementSelector); - case ShapedQueryExpression shapedQueryExpression: if (shapedQueryExpression.ResultCardinality == ResultCardinality.Enumerable) { @@ -548,180 +545,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } - // GroupBy Aggregate case - if (methodCallExpression.Object == null - && methodCallExpression.Method.DeclaringType == typeof(Enumerable) - && methodCallExpression.Arguments.Count > 0) - { - if (methodCallExpression.Arguments[0].Type.TryGetElementType(typeof(IQueryable<>)) == null - && Visit(methodCallExpression.Arguments[0]) is GroupingElementExpression groupingElementExpression) - { - Expression? result; - switch (methodCallExpression.Method.Name) - { - case nameof(Enumerable.Average): - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - result = GetExpressionForAggregation(groupingElementExpression) is SqlExpression averageExpression - ? TranslateAverage(averageExpression) - : null; - break; - - case nameof(Enumerable.Count): - if (methodCallExpression.Arguments.Count == 2) - { - var newGroupingElementExpression = ApplyPredicate( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - if (newGroupingElementExpression == null) - { - result = null; - break; - } - groupingElementExpression = newGroupingElementExpression; - } - - result = TranslateCount(GetExpressionForAggregation(groupingElementExpression, starProjection: true)!); - break; - - case nameof(Enumerable.Distinct): - result = groupingElementExpression.Element is EntityShaperExpression - ? groupingElementExpression - : groupingElementExpression.IsDistinct - ? null - : groupingElementExpression.ApplyDistinct(); - break; - - case nameof(Enumerable.LongCount): - if (methodCallExpression.Arguments.Count == 2) - { - var newGroupingElementExpression = ApplyPredicate( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - if (newGroupingElementExpression == null) - { - result = null; - break; - } - groupingElementExpression = newGroupingElementExpression; - } - - result = TranslateLongCount(GetExpressionForAggregation(groupingElementExpression, starProjection: true)!); - break; - - case nameof(Enumerable.Max): - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - result = GetExpressionForAggregation(groupingElementExpression) is SqlExpression maxExpression - ? TranslateMax(maxExpression) - : null; - break; - - case nameof(Enumerable.Min): - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - result = GetExpressionForAggregation(groupingElementExpression) is SqlExpression minExpression - ? TranslateMin(minExpression) - : null; - break; - - case nameof(Enumerable.Select): - result = ApplySelector(groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - break; - - case nameof(Enumerable.Sum): - if (methodCallExpression.Arguments.Count == 2) - { - groupingElementExpression = ApplySelector( - groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - } - - result = GetExpressionForAggregation(groupingElementExpression) is SqlExpression sumExpression - ? TranslateSum(sumExpression) - : null; - break; - - case nameof(Enumerable.Where): - result = ApplyPredicate(groupingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); - break; - - default: - result = null; - break; - } - - return result ?? throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); - - GroupingElementExpression? ApplyPredicate(GroupingElementExpression groupingElement, LambdaExpression lambdaExpression) - { - var predicate = TranslateInternal(RemapLambda(groupingElement, lambdaExpression)); - - return predicate == null - ? null - : groupingElement.ApplyPredicate(predicate); - } - - static GroupingElementExpression ApplySelector( - GroupingElementExpression groupingElement, - LambdaExpression lambdaExpression) - { - var selector = RemapLambda(groupingElement, lambdaExpression); - - return groupingElement.ApplySelector(selector); - } - - static Expression RemapLambda(GroupingElementExpression groupingElement, LambdaExpression lambdaExpression) - => ReplacingExpressionVisitor.Replace( - lambdaExpression.Parameters[0], groupingElement.Element, lambdaExpression.Body); - - SqlExpression? GetExpressionForAggregation(GroupingElementExpression groupingElement, bool starProjection = false) - { - var selector = TranslateInternal(groupingElement.Element); - if (selector == null) - { - if (starProjection) - { - selector = _sqlExpressionFactory.Fragment("*"); - } - else - { - return null; - } - } - - if (groupingElement.Predicate != null) - { - if (selector is SqlFragmentExpression) - { - selector = _sqlExpressionFactory.Constant(1); - } - - selector = _sqlExpressionFactory.Case( - new List { new(groupingElement.Predicate, selector) }, - elseResult: null); - } - - if (groupingElement.IsDistinct - && !(selector is SqlFragmentExpression)) - { - selector = new DistinctExpression(selector); - } - - return selector; - } - } - } - // Subquery case var subqueryTranslation = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(methodCallExpression); if (subqueryTranslation != null) @@ -1551,61 +1374,6 @@ public Expression Convert(Type type) } } - private sealed class GroupingElementExpression : Expression - { - public GroupingElementExpression(Expression element) - { - Element = element; - } - - public Expression Element { get; private set; } - public bool IsDistinct { get; private set; } - public SqlExpression? Predicate { get; private set; } - - public GroupingElementExpression ApplyDistinct() - { - IsDistinct = true; - - return this; - } - - public GroupingElementExpression ApplySelector(Expression expression) - { - Element = expression; - - return this; - } - - public GroupingElementExpression ApplyPredicate(SqlExpression expression) - { - Check.NotNull(expression, nameof(expression)); - - if (expression is SqlConstantExpression sqlConstant - && sqlConstant.Value is bool boolValue - && boolValue) - { - return this; - } - - Predicate = Predicate == null - ? expression - : new SqlBinaryExpression( - ExpressionType.AndAlso, - Predicate, - expression, - typeof(bool), - expression.TypeMapping); - - return this; - } - - public override Type Type - => typeof(IEnumerable<>).MakeGenericType(Element.Type); - - public override ExpressionType NodeType - => ExpressionType.Extension; - } - private sealed class SqlTypeMappingVerifyingExpressionVisitor : ExpressionVisitor { protected override Expression VisitExtension(Expression extensionExpression) diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index 0adf12361a2..dd47a931825 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -633,7 +633,7 @@ public SelectExpressionVerifyingExpressionVisitor(IEnumerable(selectExpression._projectionMapping.Count); + foreach (var keyValuePair in selectExpression._projectionMapping) + { + newProjectionMappings[keyValuePair.Key] = Visit(keyValuePair.Value); + } + // We ignore projection binding related elements as we don't want to copy them over for top level // Nested level will have _projection populated and no binding elements var newProjections = selectExpression._projection.Select(Visit).ToList(); @@ -770,7 +776,8 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor Limit = limit, IsDistinct = selectExpression.IsDistinct, Tags = selectExpression.Tags, - _usedAliases = selectExpression._usedAliases.ToHashSet() + _usedAliases = selectExpression._usedAliases.ToHashSet(), + _projectionMapping = newProjectionMappings }; newSelectExpression._tptLeftJoinTables.AddRange(selectExpression._tptLeftJoinTables); @@ -789,7 +796,6 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor .Visit(newSelectExpression); return newSelectExpression; - } return expression is ICloneable cloneable ? (Expression)cloneable.Clone() : base.Visit(expression); @@ -812,6 +818,7 @@ public ColumnExpressionReplacingExpressionVisitor(SelectExpression oldSelectExpr { return expression is ConcreteColumnExpression concreteColumnExpression && _oldSelectExpression.ContainsTableReference(concreteColumnExpression) + && _newTableReferences.ContainsKey(concreteColumnExpression.TableAlias) ? new ConcreteColumnExpression( concreteColumnExpression.Name, _newTableReferences[concreteColumnExpression.TableAlias], @@ -821,5 +828,126 @@ public ColumnExpressionReplacingExpressionVisitor(SelectExpression oldSelectExpr : base.Visit(expression); } } + + private sealed class GroupByAggregateLiftingExpressionVisitor : ExpressionVisitor + { + private readonly SelectExpression _selectExpression; + + public GroupByAggregateLiftingExpressionVisitor(SelectExpression selectExpression) + { + _selectExpression = selectExpression; + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + if (expression is SqlExpression sqlExpression + && sqlExpression is ScalarSubqueryExpression scalarSubqueryExpression) + { + // A scalar subquery on a GROUP BY may represent aggregation which can be lifted. + var subquery = scalarSubqueryExpression.Subquery; + if (subquery.Limit == null + && subquery.Offset == null + && subquery._groupBy.Count == 0 + && subquery.Predicate != null) + { + var initialTableCounts = 0; + var potentialTableCount = Math.Min(_selectExpression._tables.Count, subquery._tables.Count); + for (var i = 0; i < potentialTableCount; i++) + { + if (!string.Equals(_selectExpression._tableReferences[i].Alias, + subquery._tableReferences[i].Alias, StringComparison.OrdinalIgnoreCase)) + { + break; + } + + if (_selectExpression._tables[i] is SelectExpression originalNestedSelectExpression + && subquery._tables[i] is SelectExpression subqueryNestedSelectExpression) + { + CopyOverOwnedJoinInSameTable(originalNestedSelectExpression, subqueryNestedSelectExpression); + } + + initialTableCounts++; + } + + if (initialTableCounts > 0) + { + // If there are no initial table then this is not correlated grouping subquery + var columnExpressionReplacingExpressionVisitor = new ColumnExpressionReplacingExpressionVisitor(subquery, _selectExpression); + if (subquery._tables.Count != initialTableCounts) + { + // If subquery has more tables then we expanded join on it. + for (var i = initialTableCounts; i < subquery._tables.Count; i++) + { + // We re-use the same table reference with updated selectExpression + // So we don't need to remap those columns, they will transfer automatically. + var table = subquery._tables[i]; + var tableReference = subquery._tableReferences[i]; + table = (TableExpressionBase)columnExpressionReplacingExpressionVisitor.Visit(table); + tableReference.UpdateTableReference(subquery, _selectExpression); + _selectExpression.AddTable(table, tableReference); + } + } + + var updatedProjection = columnExpressionReplacingExpressionVisitor.Visit(subquery._projection[0].Expression); + + return updatedProjection; + } + } + } + + if (expression is SelectExpression innerSelectExpression + && innerSelectExpression.GroupBy.Count > 0) + { + expression = new GroupByAggregateLiftingExpressionVisitor(innerSelectExpression).Visit(innerSelectExpression); + } + + return base.Visit(expression); + } + + private void CopyOverOwnedJoinInSameTable(SelectExpression target, SelectExpression source) + { + if (target._projection.Count != source._projection.Count) + { + var columnExpressionReplacingExpressionVisitor = new ColumnExpressionReplacingExpressionVisitor(source, target); + var minProjectionCount = Math.Min(target._projection.Count, source._projection.Count); + var initialProjectionCount = 0; + for (var i = 0; i < minProjectionCount; i++) + { + var projectionToCopy = source._projection[i]; + var transformedProjection = (ProjectionExpression)columnExpressionReplacingExpressionVisitor.Visit(projectionToCopy); + if (!transformedProjection.Equals(target._projection[i])) + { + break; + } + + initialProjectionCount++; + } + + if (initialProjectionCount < source._projection.Count) + { + for (var i = initialProjectionCount; i < source._projection.Count; i++) + { + var projectionToCopy = source._projection[i].Expression; + if (projectionToCopy is not ConcreteColumnExpression columnToCopy) + { + continue; + } + + var transformedProjection = (ConcreteColumnExpression)columnExpressionReplacingExpressionVisitor.Visit(projectionToCopy); + if (target._projection.FindIndex(e => e.Expression.Equals(transformedProjection)) == -1) + { + target._projection.Add(new ProjectionExpression(transformedProjection, transformedProjection.Name)); + if (UnwrapJoinExpression(columnToCopy.Table) is SelectExpression innerSelectExpression) + { + var tableIndex = source._tableReferences.FindIndex(e => e.Alias == columnToCopy.TableAlias); + CopyOverOwnedJoinInSameTable((SelectExpression)UnwrapJoinExpression(target._tables[tableIndex]), innerSelectExpression); + } + } + } + } + } + } + } } } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 656cc9793c2..ff6ed2ebb7d 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -59,6 +59,8 @@ public sealed partial class SelectExpression : TableExpressionBase private List _clientProjections = new(); private readonly List _aliasForClientProjections = new(); + private CloningExpressionVisitor? _cloningExpressionVisitor; + private SelectExpression( string? alias, List projections, @@ -1020,6 +1022,8 @@ public int AddToProjection(SqlExpression sqlExpression) private int AddToProjection(SqlExpression sqlExpression, string? alias, bool assignUniqueTableAlias = true) { + sqlExpression = TryLiftGroupByAggregate(sqlExpression); + var existingIndex = _projection.FindIndex(pe => pe.Expression.Equals(sqlExpression)); if (existingIndex != -1) { @@ -1055,12 +1059,12 @@ private int AddToProjection(SqlExpression sqlExpression, string? alias, bool ass /// /// Applies filter predicate to the . /// - /// An expression to use for filtering. - public void ApplyPredicate(SqlExpression expression) + /// An expression to use for filtering. + public void ApplyPredicate(SqlExpression sqlExpression) { - Check.NotNull(expression, nameof(expression)); + Check.NotNull(sqlExpression, nameof(sqlExpression)); - if (expression is SqlConstantExpression sqlConstant + if (sqlExpression is SqlConstantExpression sqlConstant && sqlConstant.Value is bool boolValue && boolValue) { @@ -1070,32 +1074,33 @@ public void ApplyPredicate(SqlExpression expression) if (Limit != null || Offset != null) { - expression = PushdownIntoSubqueryInternal().Remap(expression); + sqlExpression = PushdownIntoSubqueryInternal().Remap(sqlExpression); } - expression = AssignUniqueAliases(expression); + sqlExpression = TryLiftGroupByAggregate(sqlExpression); + sqlExpression = AssignUniqueAliases(sqlExpression); if (_groupBy.Count > 0) { Having = Having == null - ? expression + ? sqlExpression : new SqlBinaryExpression( ExpressionType.AndAlso, Having, - expression, + sqlExpression, typeof(bool), - expression.TypeMapping); + sqlExpression.TypeMapping); } else { Predicate = Predicate == null - ? expression + ? sqlExpression : new SqlBinaryExpression( ExpressionType.AndAlso, Predicate, - expression, + sqlExpression, typeof(bool), - expression.TypeMapping); + sqlExpression.TypeMapping); } } @@ -1144,6 +1149,14 @@ public Expression ApplyGrouping(Expression keySelector) return keySelector; } + /// + /// Clears existing group by terms. + /// + public void ClearGroupBy() + { + _groupBy.Clear(); + } + private void AppendGroupBy(Expression keySelector, List groupByTerms, List groupByAliases, string? name) { Check.NotNull(keySelector, nameof(keySelector)); @@ -1198,7 +1211,7 @@ public void ApplyOrdering(OrderingExpression orderingExpression) } _orderings.Clear(); - _orderings.Add(orderingExpression.Update(AssignUniqueAliases(orderingExpression.Expression))); + AppendOrdering(orderingExpression); } /// @@ -1209,6 +1222,12 @@ public void AppendOrdering(OrderingExpression orderingExpression) { Check.NotNull(orderingExpression, nameof(orderingExpression)); + if (_groupBy.Count > 0) + { + orderingExpression = orderingExpression.Update( + (SqlExpression)new GroupByAggregateLiftingExpressionVisitor(this).Visit(orderingExpression.Expression)); + } + if (_orderings.FirstOrDefault(o => o.Expression.Equals(orderingExpression.Expression)) == null) { _orderings.Add(orderingExpression.Update(AssignUniqueAliases(orderingExpression.Expression))); @@ -1385,20 +1404,6 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi select2.PushdownIntoSubquery(); select2.ClearOrdering(); } - // select1 already has unique aliases. We unique-fy select2 and set operation alias. - select2 = (SelectExpression)new AliasUniquefier(_usedAliases).Visit(select2); - var setOperationAlias = GenerateUniqueAlias(_usedAliases, "t"); - - var setExpression = setOperationType switch - { - SetOperationType.Except => (SetOperationBase)new ExceptExpression(setOperationAlias, select1, select2, distinct), - SetOperationType.Intersect => new IntersectExpression(setOperationAlias, select1, select2, distinct), - SetOperationType.Union => new UnionExpression(setOperationAlias, select1, select2, distinct), - _ => throw new InvalidOperationException(CoreStrings.InvalidSwitch(nameof(setOperationType), setOperationType)) - }; - var tableReferenceExpression = new TableReferenceExpression(this, setExpression.Alias); - _tables.Add(setExpression); - _tableReferences.Add(tableReferenceExpression); if (_clientProjections.Count > 0 || select2._clientProjections.Count > 0) @@ -1413,6 +1418,9 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi throw new InvalidOperationException(RelationalStrings.ProjectionMappingCountMismatch); } + var setOperationAlias = GenerateUniqueAlias(_usedAliases, "t"); + var tableReferenceExpression = new TableReferenceExpression(this, setOperationAlias); + var aliasUniquefier = new AliasUniquefier(_usedAliases); foreach (var joinedMapping in select1._projectionMapping.Join( select2._projectionMapping, @@ -1427,9 +1435,7 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi continue; } - // We have to unique-fy left side since those projections were never uniquefied - // Right side is unique already when we did it when running select2 through it. - var innerColumn1 = (SqlExpression)aliasUniquefier.Visit(joinedMapping.Value1); + var innerColumn1 = (SqlExpression)joinedMapping.Value1; var innerColumn2 = (SqlExpression)joinedMapping.Value2; // For now, make sure that both sides output the same store type, otherwise the query may fail. // TODO: with #15586 we'll be able to also allow different store types which are implicitly convertible to one another. @@ -1438,6 +1444,13 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi throw new InvalidOperationException(RelationalStrings.SetOperationsOnDifferentStoreTypes); } + innerColumn1 = select1.TryLiftGroupByAggregate(innerColumn1); + innerColumn2 = select2.TryLiftGroupByAggregate(innerColumn2); + + // We have to unique-fy left side since those projections were never uniquefied + // Right side is unique already when we did it when running select2 through it. + innerColumn1 = (SqlExpression)aliasUniquefier.Visit(innerColumn1); + var alias = GenerateUniqueColumnAlias( joinedMapping.Key.Last?.Name ?? (innerColumn1 as ColumnExpression)?.Name @@ -1479,6 +1492,19 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi } } + // We generate actual set operation after applying projection to lift group by aggregate + // select1 already has unique aliases. We unique-fy select2 and set operation alias. + select2 = (SelectExpression)aliasUniquefier.Visit(select2); + var setExpression = setOperationType switch + { + SetOperationType.Except => (SetOperationBase)new ExceptExpression(setOperationAlias, select1, select2, distinct), + SetOperationType.Intersect => new IntersectExpression(setOperationAlias, select1, select2, distinct), + SetOperationType.Union => new UnionExpression(setOperationAlias, select1, select2, distinct), + _ => throw new InvalidOperationException(CoreStrings.InvalidSwitch(nameof(setOperationType), setOperationType)) + }; + _tables.Add(setExpression); + _tableReferences.Add(tableReferenceExpression); + // We should apply _identifiers only when it is distinct and actual select expression had identifiers. if (distinct && outerIdentifiers.Length > 0) @@ -1785,7 +1811,6 @@ static TableReferenceExpression FindTableReference(SelectExpression selectExpres } } - private enum JoinType { InnerJoin, @@ -2850,6 +2875,23 @@ public void PrepareForAggregate() } } + /// + /// 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 SelectExpression Clone() + { + if (_cloningExpressionVisitor == null) + { + _cloningExpressionVisitor = new(); + } + + return (SelectExpression)_cloningExpressionVisitor.Visit(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 @@ -3003,6 +3045,10 @@ private bool ContainsTableReference(ColumnExpression column) // At that point aliases are not unique-fied across so we need to match tables => Tables.Any(e => ReferenceEquals(e, column.Table)); + private SqlExpression TryLiftGroupByAggregate(SqlExpression sqlExpression) + => _groupBy.Count > 0 + ? (SqlExpression)new GroupByAggregateLiftingExpressionVisitor(this).Visit(sqlExpression) + : sqlExpression; private void AddTable(TableExpressionBase tableExpressionBase, TableReferenceExpression tableReferenceExpression) { diff --git a/src/EFCore/Query/GroupByShaperExpression.cs b/src/EFCore/Query/GroupByShaperExpression.cs index c4ea98dfd96..80f080efbcd 100644 --- a/src/EFCore/Query/GroupByShaperExpression.cs +++ b/src/EFCore/Query/GroupByShaperExpression.cs @@ -23,16 +23,16 @@ public class GroupByShaperExpression : Expression, IPrintableExpression /// Creates a new instance of the class. /// /// An expression representing key selector for the grouping element. - /// An expression representing element selector for the grouping element. + /// A expression representing element selector for the grouping element. public GroupByShaperExpression( Expression keySelector, - Expression elementSelector) + ShapedQueryExpression groupingEnumerable) { Check.NotNull(keySelector, nameof(keySelector)); - Check.NotNull(elementSelector, nameof(elementSelector)); + Check.NotNull(groupingEnumerable, nameof(groupingEnumerable)); KeySelector = keySelector; - ElementSelector = elementSelector; + GroupingEnumerable = groupingEnumerable; } /// @@ -43,11 +43,11 @@ public class GroupByShaperExpression : Expression, IPrintableExpression /// /// The expression representing the element selector for this grouping element. /// - public virtual Expression ElementSelector { get; } + public virtual ShapedQueryExpression GroupingEnumerable { get; } /// public override Type Type - => typeof(IGrouping<,>).MakeGenericType(KeySelector.Type, ElementSelector.Type); + => typeof(IGrouping<,>).MakeGenericType(KeySelector.Type, GroupingEnumerable.ShaperExpression.Type); /// public sealed override ExpressionType NodeType @@ -59,9 +59,9 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) Check.NotNull(visitor, nameof(visitor)); var keySelector = visitor.Visit(KeySelector); - var elementSelector = visitor.Visit(ElementSelector); + var groupingEnumerable = (ShapedQueryExpression)visitor.Visit(GroupingEnumerable); - return Update(keySelector, elementSelector); + return Update(keySelector, groupingEnumerable); } /// @@ -69,15 +69,15 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// return this expression. /// /// 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 GroupByShaperExpression Update(Expression keySelector, Expression elementSelector) + public virtual GroupByShaperExpression Update(Expression keySelector, ShapedQueryExpression groupingEnumerable) { Check.NotNull(keySelector, nameof(keySelector)); - Check.NotNull(elementSelector, nameof(elementSelector)); + Check.NotNull(groupingEnumerable, nameof(groupingEnumerable)); - return keySelector != KeySelector || elementSelector != ElementSelector - ? new GroupByShaperExpression(keySelector, elementSelector) + return keySelector != KeySelector || groupingEnumerable != GroupingEnumerable + ? new GroupByShaperExpression(keySelector, groupingEnumerable) : this; } @@ -90,8 +90,8 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) expressionPrinter.Append("KeySelector: "); expressionPrinter.Visit(KeySelector); expressionPrinter.AppendLine(", "); - expressionPrinter.Append("ElementSelector:"); - expressionPrinter.Visit(ElementSelector); + expressionPrinter.Append("GroupingEnumerable:"); + expressionPrinter.Visit(GroupingEnumerable); expressionPrinter.AppendLine(); } } diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index e429142d98b..daba7d2903c 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -1064,6 +1064,111 @@ protected override Expression VisitExtension(Expression extensionExpression) } } + private sealed class CloningExpressionVisitor : ExpressionVisitor + { + private readonly Dictionary _clonedMap = new(ReferenceEqualityComparer.Instance); + + public NavigationTreeNode Clone(NavigationTreeNode navigationTreeNode) + { + _clonedMap.Clear(); + + return (NavigationTreeNode)Visit(navigationTreeNode); + } + + public IReadOnlyDictionary ClonedNodesMap => _clonedMap; + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + switch (expression) + { + case EntityReference entityReference: + return entityReference.Snapshot(); + + case NavigationTreeExpression navigationTreeExpression: + if (!_clonedMap.TryGetValue(navigationTreeExpression, out var clonedNavigationTreeExpression)) + { + clonedNavigationTreeExpression = new NavigationTreeExpression(Visit(navigationTreeExpression.Value)); + _clonedMap[navigationTreeExpression] = clonedNavigationTreeExpression; + } + + return clonedNavigationTreeExpression; + + case NavigationTreeNode navigationTreeNode: + if (!_clonedMap.TryGetValue(navigationTreeNode, out var clonedNavigationTreeNode)) + { + clonedNavigationTreeNode = new NavigationTreeNode( + (NavigationTreeNode)Visit(navigationTreeNode.Left!), + (NavigationTreeNode)Visit(navigationTreeNode.Right!)); + _clonedMap[navigationTreeNode] = clonedNavigationTreeNode; + } + + return clonedNavigationTreeNode; + + default: + return base.Visit(expression); + } + } + } + + private sealed class GroupingElementReplacingExpressionVisitor : ExpressionVisitor + { + private readonly CloningExpressionVisitor _cloningExpressionVisitor; + private readonly ParameterExpression _parameterExpression; + private readonly NavigationExpansionExpression _navigationExpansionExpression; + private readonly Expression? _keyAccessExpression; + private readonly MemberInfo? _keyMemberInfo; + + public GroupingElementReplacingExpressionVisitor( + ParameterExpression parameterExpression, + GroupByNavigationExpansionExpression groupByNavigationExpansionExpression) + { + _parameterExpression = parameterExpression; + _navigationExpansionExpression = (NavigationExpansionExpression)groupByNavigationExpansionExpression.GroupingEnumerable; + _keyAccessExpression = Expression.MakeMemberAccess(groupByNavigationExpansionExpression.CurrentParameter, + groupByNavigationExpansionExpression.CurrentParameter.Type.GetRequiredDeclaredProperty(nameof(IGrouping.Key))); + _keyMemberInfo = parameterExpression.Type.GetRequiredDeclaredProperty(nameof(IGrouping.Key)); + _cloningExpressionVisitor = new CloningExpressionVisitor(); + } + + public GroupingElementReplacingExpressionVisitor( + ParameterExpression parameterExpression, + NavigationExpansionExpression navigationExpansionExpression) + { + _parameterExpression = parameterExpression; + _navigationExpansionExpression = navigationExpansionExpression; + _cloningExpressionVisitor = new CloningExpressionVisitor(); + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + if (expression == _parameterExpression) + { + var currentTree = _cloningExpressionVisitor.Clone(_navigationExpansionExpression.CurrentTree); + + return new NavigationExpansionExpression( + _navigationExpansionExpression.Source, + currentTree, + new ReplacingExpressionVisitor( + _cloningExpressionVisitor.ClonedNodesMap.Keys.ToList(), + _cloningExpressionVisitor.ClonedNodesMap.Values.ToList()) + .Visit(_navigationExpansionExpression.PendingSelector), + _navigationExpansionExpression.CurrentParameter.Name!); + } + + return base.Visit(expression); + } + + protected override Expression VisitMember(MemberExpression memberExpression) + { + return memberExpression.Member == _keyMemberInfo + && memberExpression.Expression == _parameterExpression + ? _keyAccessExpression! + : base.VisitMember(memberExpression); + } + } + private sealed class RemoveRedundantNavigationComparisonExpressionVisitor : ExpressionVisitor { private readonly IDiagnosticsLogger _logger; diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index 2f917618133..f2c4a5f044b 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -24,8 +24,7 @@ public EntityReference(IEntityType entityType, QueryRootExpression? queryRootExp public IEntityType EntityType { get; } - public IDictionary<(IForeignKey, bool), Expression> ForeignKeyExpansionMap { get; } = - new Dictionary<(IForeignKey, bool), Expression>(); + public Dictionary<(IForeignKey, bool), Expression> ForeignKeyExpansionMap { get; } = new(); public bool IsOptional { get; private set; } public IncludeTreeNode IncludePaths { get; private set; } @@ -314,6 +313,68 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) } } + private sealed class GroupByNavigationExpansionExpression : Expression, IPrintableExpression + { + public GroupByNavigationExpansionExpression( + Expression source, + ParameterExpression groupingParameter, + NavigationTreeNode currentTree, + Expression pendingSelector, + string innerParameterName) + { + Source = source; + CurrentParameter = groupingParameter; + Type = source.Type; + GroupingEnumerable = new NavigationExpansionExpression( + Call(QueryableMethods.AsQueryable.MakeGenericMethod(CurrentParameter.Type.GetGenericArguments()[1]), CurrentParameter), + currentTree, + pendingSelector, + innerParameterName); + } + + public Expression Source { get; private set; } + + public ParameterExpression CurrentParameter { get; } + + public Expression GroupingEnumerable { get; private set; } + + public Type SourceElementType + => CurrentParameter.Type; + + public void UpdateSource(Expression source) + { + Source = source; + } + + public override ExpressionType NodeType + => ExpressionType.Extension; + + public override Type Type { get; } + + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + return this; + } + + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + { + Check.NotNull(expressionPrinter, nameof(expressionPrinter)); + + expressionPrinter.AppendLine(nameof(GroupByNavigationExpansionExpression)); + using (expressionPrinter.Indent()) + { + expressionPrinter.Append("Source: "); + expressionPrinter.Visit(Source); + expressionPrinter.AppendLine(); + expressionPrinter.Append("GroupingEnumerable: "); + expressionPrinter.Visit(GroupingEnumerable); + expressionPrinter.AppendLine(); + } + } + } + /// /// A leaf node on navigation tree, representing projection structures of /// . Contains , diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index abfc4c0a9f0..4730ada9dd0 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -273,10 +273,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp case nameof(Queryable.Any) when genericMethod == QueryableMethods.AnyWithoutPredicate: - case nameof(Queryable.Count) when genericMethod == QueryableMethods.CountWithoutPredicate: - case nameof(Queryable.LongCount) when genericMethod == QueryableMethods.LongCountWithoutPredicate: return ProcessAllAnyCountLongCount( @@ -567,15 +565,143 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } + if (firstArgument is GroupByNavigationExpansionExpression groupBySource) + { + switch (method.Name) + { + case nameof(Queryable.AsQueryable) + when genericMethod == QueryableMethods.AsQueryable: + return groupBySource; + + case nameof(Queryable.Any) + when genericMethod == QueryableMethods.AnyWithoutPredicate: + case nameof(Queryable.Count) + when genericMethod == QueryableMethods.CountWithoutPredicate: + case nameof(Queryable.LongCount) + when genericMethod == QueryableMethods.LongCountWithoutPredicate: + return ProcessAllAnyCountLongCount( + groupBySource, + genericMethod, + predicate: null); + + case nameof(Queryable.All) + when genericMethod == QueryableMethods.All: + case nameof(Queryable.Any) + when genericMethod == QueryableMethods.AnyWithPredicate: + case nameof(Queryable.Count) + when genericMethod == QueryableMethods.CountWithPredicate: + case nameof(Queryable.LongCount) + when genericMethod == QueryableMethods.LongCountWithPredicate: + return ProcessAllAnyCountLongCount( + groupBySource, + genericMethod, + methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + + // case nameof(Queryable.Average) + // when QueryableMethods.IsAverageWithoutSelector(method): + // case nameof(Queryable.Max) + // when genericMethod == QueryableMethods.MaxWithoutSelector: + // case nameof(Queryable.Min) + // when genericMethod == QueryableMethods.MinWithoutSelector: + // case nameof(Queryable.Sum) + // when QueryableMethods.IsSumWithoutSelector(method): + // return ProcessAverageMaxMinSum( + // groupBySource, + // genericMethod ?? method, + // selector: null); + + // case nameof(Queryable.Average) + // when QueryableMethods.IsAverageWithSelector(method): + // case nameof(Queryable.Sum) + // when QueryableMethods.IsSumWithSelector(method): + // case nameof(Queryable.Max) + // when genericMethod == QueryableMethods.MaxWithSelector: + // case nameof(Queryable.Min) + // when genericMethod == QueryableMethods.MinWithSelector: + // return ProcessAverageMaxMinSum( + // groupBySource, + // genericMethod ?? method, + // methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + + case nameof(Queryable.OrderBy) + when genericMethod == QueryableMethods.OrderBy: + case nameof(Queryable.OrderByDescending) + when genericMethod == QueryableMethods.OrderByDescending: + return ProcessOrderByThenBy( + groupBySource, + genericMethod, + methodCallExpression.Arguments[1].UnwrapLambdaFromQuote(), + thenBy: false); + + case nameof(Queryable.ThenBy) + when genericMethod == QueryableMethods.ThenBy: + case nameof(Queryable.ThenByDescending) + when genericMethod == QueryableMethods.ThenByDescending: + return ProcessOrderByThenBy( + groupBySource, + genericMethod, + methodCallExpression.Arguments[1].UnwrapLambdaFromQuote(), + thenBy: true); + + case nameof(Queryable.Select) + when genericMethod == QueryableMethods.Select: + return ProcessSelect( + groupBySource, + methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + + case nameof(Queryable.Skip) + when genericMethod == QueryableMethods.Skip: + case nameof(Queryable.Take) + when genericMethod == QueryableMethods.Take: + return ProcessSkipTake( + groupBySource, + genericMethod, + methodCallExpression.Arguments[1]); + + case nameof(Queryable.Where) + when genericMethod == QueryableMethods.Where: + return ProcessWhere( + groupBySource, + methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + + default: + // Average/Max/Min/Sum + // Distinct + // Contains + // First/Single/Last(OrDefault) + // Join, LeftJoin, GroupJoin + // SelectMany + // Concat/Except/Intersect/Union + // Cast/OfType + // Include/ThenInclude/NotQuiteInclude + // GroupBy + // Reverse + // DefaultIfEmpty + throw new InvalidOperationException( + CoreStrings.TranslationFailed( + _reducingExpressionVisitor.Visit(methodCallExpression).Print())); + } + } + if (genericMethod == QueryableMethods.AsQueryable) { + if (firstArgument is NavigationTreeExpression navigationTreeExpression + && navigationTreeExpression.Type.IsGenericType + && navigationTreeExpression.Type.GetGenericTypeDefinition() == typeof(IGrouping<,>)) + { + // This is groupingElement.AsQueryable so we preserve it + return Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(navigationTreeExpression.Type.GetSequenceType()), + navigationTreeExpression); + } + return UnwrapCollectionMaterialization(firstArgument); } if (firstArgument.Type.TryGetElementType(typeof(IQueryable<>)) == null) { // firstArgument was not an queryable - var visitedArguments = new [] { firstArgument } + var visitedArguments = new[] { firstArgument } .Concat(methodCallExpression.Arguments.Skip(1).Select(e => Visit(e)!)); return ConvertToEnumerable(method, visitedArguments); @@ -799,13 +925,15 @@ private NavigationExpansionExpression ProcessDistinct(NavigationExpansionExpress return source; } - private NavigationExpansionExpression ProcessGroupBy( + // This returns Expression since it can also return a deferred GroupBy operation + private Expression ProcessGroupBy( NavigationExpansionExpression source, LambdaExpression keySelector, LambdaExpression? elementSelector, LambdaExpression? resultSelector) { var keySelectorBody = ExpandNavigationsForSource(source, RemapLambdaExpression(source, keySelector)); + // Need to generate lambda after processing element/result selector Expression result; if (elementSelector != null) @@ -813,26 +941,43 @@ private NavigationExpansionExpression ProcessDistinct(NavigationExpansionExpress source = ProcessSelect(source, elementSelector); } - source = (NavigationExpansionExpression)_pendingSelectorExpandingExpressionVisitor.Visit(source); - // TODO: Flow include in future - //source = (NavigationExpansionExpression)new IncludeApplyingExpressionVisitor( - // this, _queryCompilationContext.IsTracking).Visit(source); keySelector = GenerateLambda(keySelectorBody, source.CurrentParameter); - elementSelector = GenerateLambda(source.PendingSelector, source.CurrentParameter); - result = resultSelector == null - ? Expression.Call( - QueryableMethods.GroupByWithKeyElementSelector.MakeGenericMethod( - source.CurrentParameter.Type, keySelector.ReturnType, elementSelector.ReturnType), + var innerParameterName = GetParameterName("e"); + + if (resultSelector == null) + { + var groupingParameter = Expression.Parameter( + typeof(IGrouping<,>).MakeGenericType(keySelector.ReturnType, source.SourceElementType), + GetParameterName("g")); + var innerSource = Expression.Call( + QueryableMethods.GroupByWithKeySelector.MakeGenericMethod(source.SourceElementType, keySelector.ReturnType), source.Source, - Expression.Quote(keySelector), - Expression.Quote(elementSelector)) - : Expression.Call( - QueryableMethods.GroupByWithKeyElementResultSelector.MakeGenericMethod( - source.CurrentParameter.Type, keySelector.ReturnType, elementSelector.ReturnType, resultSelector.ReturnType), + Expression.Quote(keySelector)); + + return new GroupByNavigationExpansionExpression(innerSource, groupingParameter, source.CurrentTree, source.PendingSelector, innerParameterName); + } + + var enumerableParameter = Expression.Parameter( + typeof(IEnumerable<>).MakeGenericType(source.SourceElementType), + GetParameterName("g")); + var groupingEnumerable = new NavigationExpansionExpression( + Expression.Call(QueryableMethods.AsQueryable.MakeGenericMethod(source.SourceElementType), enumerableParameter), + source.CurrentTree, + source.PendingSelector, + innerParameterName); + + var resultSelectorBody = new GroupingElementReplacingExpressionVisitor( + resultSelector.Parameters[1], groupingEnumerable).Visit(resultSelector.Body); + + resultSelectorBody = Visit(resultSelectorBody); + resultSelector = Expression.Lambda(resultSelectorBody, resultSelector.Parameters[0], enumerableParameter); + + result = Expression.Call( + QueryableMethods.GroupByWithKeyResultSelector.MakeGenericMethod( + source.CurrentParameter.Type, keySelector.ReturnType, resultSelector.ReturnType), source.Source, Expression.Quote(keySelector), - Expression.Quote(elementSelector), - Expression.Quote(Visit(resultSelector))); + Expression.Quote(resultSelector)); var navigationTree = new NavigationTreeExpression(Expression.Default(result.Type.GetSequenceType())); var parameterName = GetParameterName("e"); @@ -1116,24 +1261,6 @@ private Expression ProcessReverse(NavigationExpansionExpression source) private NavigationExpansionExpression ProcessSelect(NavigationExpansionExpression source, LambdaExpression selector) { - // This is to apply aggregate operator on GroupBy right away rather than deferring - if (source.SourceElementType.IsGenericType - && source.SourceElementType.GetGenericTypeDefinition() == typeof(IGrouping<,>) - && !(selector.ReturnType.IsGenericType - && selector.ReturnType.GetGenericTypeDefinition() == typeof(IGrouping<,>))) - { - var selectorLambda = ProcessLambdaExpression(source, selector); - var newSource = Expression.Call( - QueryableMethods.Select.MakeGenericMethod(source.SourceElementType, selectorLambda.ReturnType), - source.Source, - Expression.Quote(selectorLambda)); - - var navigationTree = new NavigationTreeExpression(Expression.Default(selectorLambda.ReturnType)); - var parameterName = GetParameterName("e"); - - return new NavigationExpansionExpression(newSource, navigationTree, navigationTree, parameterName); - } - var selectorBody = ReplacingExpressionVisitor.Replace( selector.Parameters[0], source.PendingSelector, @@ -1289,6 +1416,80 @@ private NavigationExpansionExpression ProcessWhere(NavigationExpansionExpression return source; } + private Expression ProcessAllAnyCountLongCount( + GroupByNavigationExpansionExpression groupBySource, + MethodInfo genericMethod, + LambdaExpression? predicate) + { + if (predicate != null) + { + predicate = ProcessLambdaExpression(groupBySource, predicate); + + return Expression.Call( + genericMethod.MakeGenericMethod(groupBySource.SourceElementType), groupBySource.Source, Expression.Quote(predicate)); + } + + return Expression.Call(genericMethod.MakeGenericMethod(groupBySource.SourceElementType), groupBySource.Source); + } + + private GroupByNavigationExpansionExpression ProcessOrderByThenBy( + GroupByNavigationExpansionExpression groupBySource, + MethodInfo genericMethod, + LambdaExpression keySelector, + bool thenBy) + { + keySelector = ProcessLambdaExpression(groupBySource, keySelector); + + groupBySource.UpdateSource( + Expression.Call( + genericMethod.MakeGenericMethod(groupBySource.SourceElementType, keySelector.ReturnType), + groupBySource.Source, + Expression.Quote(keySelector))); + + return groupBySource; + } + + private NavigationExpansionExpression ProcessSelect(GroupByNavigationExpansionExpression groupBySource, LambdaExpression selector) + { + var selectorBody = new GroupingElementReplacingExpressionVisitor(selector.Parameters[0], groupBySource).Visit(selector.Body); + selectorBody = Visit(selectorBody); + selectorBody = new PendingSelectorExpandingExpressionVisitor(this, _extensibilityHelper, applyIncludes: true).Visit(selectorBody); + selectorBody = Reduce(selectorBody); + selector = Expression.Lambda(selectorBody, groupBySource.CurrentParameter); + + var newSource = Expression.Call( + QueryableMethods.Select.MakeGenericMethod(groupBySource.SourceElementType, selector.ReturnType), + groupBySource.Source, + Expression.Quote(selector)); + + var navigationTree = new NavigationTreeExpression(Expression.Default(selector.ReturnType)); + var parameterName = GetParameterName("e"); + + return new NavigationExpansionExpression(newSource, navigationTree, navigationTree, parameterName); + } + + private GroupByNavigationExpansionExpression ProcessSkipTake( + GroupByNavigationExpansionExpression groupBySource, + MethodInfo genericMethod, + Expression count) + { + groupBySource.UpdateSource(Expression.Call(genericMethod.MakeGenericMethod(groupBySource.SourceElementType), groupBySource.Source, count)); + + return groupBySource; + } + + private GroupByNavigationExpansionExpression ProcessWhere(GroupByNavigationExpansionExpression groupBySource, LambdaExpression predicate) + { + predicate = ProcessLambdaExpression(groupBySource, predicate); + groupBySource.UpdateSource( + Expression.Call( + QueryableMethods.Where.MakeGenericMethod(groupBySource.SourceElementType), + groupBySource.Source, + Expression.Quote(predicate))); + + return groupBySource; + } + private void ApplyPendingOrderings(NavigationExpansionExpression source) { if (source.PendingOrderings.Any()) @@ -1664,6 +1865,13 @@ private Expression RemapLambdaExpression(NavigationExpansionExpression source, L private LambdaExpression ProcessLambdaExpression(NavigationExpansionExpression source, LambdaExpression lambdaExpression) => GenerateLambda(ExpandNavigationsForSource(source, RemapLambdaExpression(source, lambdaExpression)), source.CurrentParameter); + private LambdaExpression ProcessLambdaExpression(GroupByNavigationExpansionExpression groupBySource, LambdaExpression lambdaExpression) + { + return Expression.Lambda( + Visit(new GroupingElementReplacingExpressionVisitor(lambdaExpression.Parameters[0], groupBySource).Visit(lambdaExpression.Body)), + groupBySource.CurrentParameter); + } + private static IEnumerable FindNavigations(IEntityType entityType, string navigationName) { var navigation = entityType.FindNavigation(navigationName); diff --git a/src/EFCore/Query/ProjectionMember.cs b/src/EFCore/Query/ProjectionMember.cs index 14a8cdb88ff..3e3805770eb 100644 --- a/src/EFCore/Query/ProjectionMember.cs +++ b/src/EFCore/Query/ProjectionMember.cs @@ -81,6 +81,7 @@ public ProjectionMember Prepend(MemberInfo member) => _memberChain.LastOrDefault(); /// + [DebuggerStepThrough] public override int GetHashCode() { var hash = new HashCode(); @@ -94,6 +95,7 @@ public override int GetHashCode() } /// + [DebuggerStepThrough] public override bool Equals(object? obj) => obj != null && (obj is ProjectionMember projectionMember diff --git a/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs b/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs index aa37b8cc649..c157250c775 100644 --- a/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs +++ b/test/EFCore.Relational.Specification.Tests/TestUtilities/TestSqlLoggerFactory.cs @@ -21,7 +21,7 @@ public class TestSqlLoggerFactory : ListLoggerFactory private static readonly string _eol = Environment.NewLine; - private static object _queryBaselineFileLock = new(); + private static readonly object _queryBaselineFileLock = new(); public TestSqlLoggerFactory() : this(_ => true) @@ -129,7 +129,7 @@ public TestSqlLogger(bool shouldLogCommands) public List SqlStatements { get; } = new(); public List Parameters { get; } = new(); - private StringBuilder _stringBuilder = new(); + private readonly StringBuilder _stringBuilder = new(); protected override void UnsafeClear() { diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsCollectionsQueryTestBase.cs index 2f5acadd94b..6353b847bfd 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsCollectionsQueryTestBase.cs @@ -1197,7 +1197,7 @@ public virtual Task Include_collection_with_multiple_orderbys_complex_repeated_c } } - [ConditionalTheory(Skip = "Issue#12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_collection_with_groupby_in_subquery(bool async) { @@ -1210,7 +1210,7 @@ public virtual Task Include_collection_with_groupby_in_subquery(bool async) elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(e => e.OneToMany_Optional1))); } - [ConditionalTheory(Skip = "Issue#12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_collection_with_groupby_in_subquery_and_filter_before_groupby(bool async) { @@ -1224,7 +1224,7 @@ public virtual Task Include_collection_with_groupby_in_subquery_and_filter_befor elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(e => e.OneToMany_Optional1))); } - [ConditionalTheory(Skip = "Issue#12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_collection_with_groupby_in_subquery_and_filter_after_groupby(bool async) { diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 00b4569290e..e01a8b72d77 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -2817,7 +2817,7 @@ public virtual void Entries_for_detached_entities_are_removed() context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } - [ConditionalTheory(Skip = "Issue#12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_reference_with_groupby_in_subquery(bool async) { @@ -2830,7 +2830,7 @@ public virtual Task Include_reference_with_groupby_in_subquery(bool async) elementAsserter: (e, a) => AssertInclude(e, a, new ExpectedInclude(e => e.OneToOne_Optional_FK1))); } - [ConditionalTheory(Skip = "Issue#12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Multi_include_with_groupby_in_subquery(bool async) { diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 454533ae4cd..8ad55926fd5 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -5881,7 +5881,7 @@ public virtual Task Group_by_entity_key_with_include_on_that_entity_with_key_in_ }); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Group_by_with_include_with_entity_in_result_selector(bool async) { @@ -5910,7 +5910,7 @@ public virtual Task Group_by_with_include_with_entity_in_result_selector(bool as }); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_with_group_by_and_FirstOrDefault_gets_properly_applied(bool async) { diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index fef0dd56864..36225beca3c 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -2017,17 +2017,20 @@ public virtual Task Distinct_GroupBy_OrderBy_key(bool async) assertOrder: true); } - [ConditionalTheory(Skip = "Issue #21965")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_nested_collection_with_groupby(bool async) { return AssertQuery( async, - ss => ss.Set().Where(c => c.CustomerID.StartsWith("A")) + ss => ss.Set().Where(c => c.CustomerID.StartsWith("F")) + .OrderBy(c => c.CustomerID) .Select( c => c.Orders.Any() ? c.Orders.GroupBy(o => o.OrderID).Select(g => g.Key).ToArray() - : Array.Empty())); + : Array.Empty()), + assertOrder: true, + elementAsserter: (e, a) => Assert.True(e.SequenceEqual(a))); } [ConditionalTheory] @@ -2263,7 +2266,13 @@ public virtual Task GroupBy_aggregate_over_a_subquery(bool async) async, ss => ss.Set() .GroupBy(o => o.CustomerID) - .Select(g => new { g.Key, Count = (from c in ss.Set() where c.CustomerID == g.Key select c).Count() })); + .Select(g => new { g.Key, Count = (from c in ss.Set() where c.CustomerID == g.Key select c).Count() }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + AssertEqual(e.Key, a.Key); + AssertEqual(e.Count, a.Count); + }); } [ConditionalTheory] @@ -2613,61 +2622,62 @@ public virtual Task GroupBy_group_Where_Select_Distinct_aggregate(bool async) #region GroupByWithoutAggregate - [ConditionalTheory(Skip = "Issue #18923")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_SelectMany(bool async) { - return AssertQuery( + return AssertTranslationFailed(() => AssertQuery( async, ss => ss.Set().GroupBy(c => c.City).SelectMany(g => g), - entryCount: 91); + entryCount: 91)); } - [ConditionalTheory(Skip = "Issue #18923")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task OrderBy_GroupBy_SelectMany(bool async) { - return AssertQuery( + return AssertTranslationFailed(() => AssertQuery( async, ss => ss.Set().OrderBy(o => o.OrderID) .GroupBy(o => o.CustomerID) .SelectMany(g => g), - entryCount: 830); + entryCount: 830)); } - [ConditionalTheory(Skip = "Issue #18923")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task OrderBy_GroupBy_SelectMany_shadow(bool async) { - return AssertQuery( + return AssertTranslationFailed(() => AssertQuery( async, ss => ss.Set().OrderBy(e => e.EmployeeID) .GroupBy(e => e.EmployeeID) .SelectMany(g => g) - .Select(g => EF.Property(g, "Title"))); + .Select(g => EF.Property(g, "Title")))); } - [ConditionalTheory(Skip = "Issue#17761")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_with_orderby_take_skip_distinct_followed_by_group_key_projection(bool async) { - return AssertQuery( + return AssertTranslationFailed(() => AssertQuery( async, ss => ss.Set().GroupBy(o => o.CustomerID).OrderBy(g => g.Key).Take(5).Skip(3).Distinct().Select(g => g.Key), assertOrder: true, - entryCount: 31); + entryCount: 31)); } - [ConditionalTheory(Skip = "Issue #17761")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_Distinct(bool async) { - return AssertQuery( - async, - ss => ss.Set().GroupBy(o => o.CustomerID).Distinct().Select(g => g.Key)); + return AssertTranslationFailed( + () => AssertQuery( + async, + ss => ss.Set().GroupBy(o => o.CustomerID).Distinct().Select(g => g.Key))); } - [ConditionalTheory(Skip = "Issue #18923")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_with_aggregate_through_navigation_property(bool async) { @@ -2682,7 +2692,7 @@ public virtual Task GroupBy_with_aggregate_through_navigation_property(bool asyn #region GroupBySelectFirst - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_Shadow(bool async) { @@ -2693,7 +2703,7 @@ public virtual Task GroupBy_Shadow(bool async) .Select(g => EF.Property(g.First(), "Title"))); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_Shadow2(bool async) { @@ -2701,10 +2711,11 @@ public virtual Task GroupBy_Shadow2(bool async) async, ss => ss.Set().Where(e => EF.Property(e, "Title") == "Sales Representative" && e.EmployeeID == 1) .GroupBy(e => EF.Property(e, "Title")) - .Select(g => g.First())); + .Select(g => g.First()), + entryCount: 1); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_Shadow3(bool async) { @@ -2943,7 +2954,7 @@ public virtual Task GroupBy_aggregate_followed_by_another_GroupBy_aggregate(bool # region GroupByInSubquery - [ConditionalTheory(Skip = "issue #15279")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Complex_query_with_groupBy_in_subquery1(bool async) { @@ -2967,7 +2978,7 @@ public virtual Task Complex_query_with_groupBy_in_subquery1(bool async) }); } - [ConditionalTheory(Skip = "issue #15279")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Complex_query_with_groupBy_in_subquery2(bool async) { @@ -2991,7 +3002,7 @@ public virtual Task Complex_query_with_groupBy_in_subquery2(bool async) }); } - [ConditionalTheory(Skip = "issue #15279")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Complex_query_with_groupBy_in_subquery3(bool async) { @@ -3011,7 +3022,7 @@ public virtual Task Complex_query_with_groupBy_in_subquery3(bool async) elementAsserter: (e, a) => { Assert.Equal(e.Key, a.Key); - AssertCollection(e.Subquery, a.Subquery); + AssertCollection(e.Subquery, a.Subquery, elementSorter: i => i.Sum); }); } @@ -3076,27 +3087,27 @@ public virtual Task GroupBy_scalar_aggregate_in_set_operation(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task AsEnumerable_in_subquery_for_GroupBy(bool async) { - return AssertTranslationFailed( - () => AssertQuery( - async, - ss => ss.Set() - .Where(c => c.CustomerID.StartsWith("F")) - .Select(c => new - { - Customer = c, - Orders = ss.Set() - .Where(o => o.CustomerID == c.CustomerID) - .AsEnumerable() - .GroupBy(o => o.CustomerID) - .Select(g => g.OrderByDescending(e => e.OrderDate).FirstOrDefault()) - .ToList() - }), - elementSorter: e => e.Customer.CustomerID, - elementAsserter: (e, a) => + return AssertQuery( + async, + ss => ss.Set() + .Where(c => c.CustomerID.StartsWith("F")) + .Select(c => new { - AssertEqual(e.Customer, a.Customer); - AssertCollection(e.Orders, a.Orders); - })); + Customer = c, + Orders = ss.Set() + .Where(o => o.CustomerID == c.CustomerID) + .AsEnumerable() + .GroupBy(o => o.CustomerID) + .Select(g => g.OrderByDescending(e => e.OrderDate).FirstOrDefault()) + .ToList() + }), + elementSorter: e => e.Customer.CustomerID, + elementAsserter: (e, a) => + { + AssertEqual(e.Customer, a.Customer); + AssertCollection(e.Orders, a.Orders); + }, + entryCount: 15); } #endregion diff --git a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs index e647f8de83c..9c0a7c94146 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs @@ -1273,7 +1273,7 @@ public virtual async Task Include_specified_on_non_entity_not_supported(bool asy ss => ss.Set().Select(c => new Tuple(c, 5)).Include(t => t.Item1.Orders)))).Message); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_collection_GroupBy_Select(bool async) { @@ -1283,10 +1283,11 @@ public virtual Task Include_collection_GroupBy_Select(bool async) .Where(o => o.OrderID == 10248) .Include(o => o.OrderDetails) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 4); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_reference_GroupBy_Select(bool async) { @@ -1296,10 +1297,11 @@ public virtual Task Include_reference_GroupBy_Select(bool async) .Where(o => o.OrderID == 10248) .Include(o => o.Customer) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 2); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_collection_Join_GroupBy_Select(bool async) { @@ -1314,10 +1316,11 @@ public virtual Task Include_collection_Join_GroupBy_Select(bool async) od => od.OrderID, (o, od) => o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 4); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_reference_Join_GroupBy_Select(bool async) { @@ -1332,10 +1335,11 @@ public virtual Task Include_reference_Join_GroupBy_Select(bool async) od => od.OrderID, (o, od) => o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 2); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Join_Include_collection_GroupBy_Select(bool async) { @@ -1349,10 +1353,11 @@ public virtual Task Join_Include_collection_GroupBy_Select(bool async) o => o.OrderID, (od, o) => o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 4); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Join_Include_reference_GroupBy_Select(bool async) { @@ -1365,10 +1370,11 @@ public virtual Task Join_Include_reference_GroupBy_Select(bool async) o => o.OrderID, (od, o) => o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 919); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_collection_SelectMany_GroupBy_Select(bool async) { @@ -1378,10 +1384,11 @@ public virtual Task Include_collection_SelectMany_GroupBy_Select(bool async) from od in ss.Set() select o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 4); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Include_reference_SelectMany_GroupBy_Select(bool async) { @@ -1391,10 +1398,11 @@ public virtual Task Include_reference_SelectMany_GroupBy_Select(bool async) from od in ss.Set() select o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 2); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task SelectMany_Include_collection_GroupBy_Select(bool async) { @@ -1404,10 +1412,11 @@ public virtual Task SelectMany_Include_collection_GroupBy_Select(bool async) from o in ss.Set().Include(o => o.OrderDetails) select o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 2985); } - [ConditionalTheory(Skip = "Issue #12088")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task SelectMany_Include_reference_GroupBy_Select(bool async) { @@ -1417,7 +1426,8 @@ public virtual Task SelectMany_Include_reference_GroupBy_Select(bool async) from o in ss.Set().Include(o => o.Customer) select o) .GroupBy(e => e.OrderID) - .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault())); + .Select(e => e.OrderBy(o => o.OrderID).FirstOrDefault()), + entryCount: 919); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs index c127cc898c9..d9e520879c2 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindSetOperationsQueryTestBase.cs @@ -539,49 +539,364 @@ public virtual Task Union_over_columns_with_different_nullability(bool async) } [ConditionalTheory] -#pragma warning disable xUnit1016 // MemberData must reference a public member - [MemberData(nameof(GetSetOperandTestCases))] -#pragma warning restore xUnit1016 // MemberData must reference a public member - public virtual Task Union_over_different_projection_types(bool async, string leftType, string rightType) + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_column_column(bool async) { - var (left, right) = (ExpressionGenerator(leftType), ExpressionGenerator(rightType)); - return AssertQuery(async, ss => left(ss.Set()).Union(right(ss.Set()))); + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID) + .Union(ss.Set().Select(o => o.OrderID))); + } - static Func, IQueryable> ExpressionGenerator(string expressionType) - { - switch (expressionType) - { - case "Column": - return os => os.Select(o => (object)o.OrderID); - case "Function": - return os => os - .GroupBy(o => o.OrderID) - .Select(g => (object)g.Count()); - case "Constant": - return os => os.Select(o => (object)8); - case "Unary": - return os => os.Select(o => (object)-o.OrderID); - case "Binary": - return os => os.Select(o => (object)(o.OrderID + 1)); - case "ScalarSubquery": - return os => os.Select(o => (object)o.OrderDetails.Count()); - default: - throw new InvalidOperationException(); - } - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_column_function(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID) + .Union(ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_column_constant(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID) + .Union(ss.Set().Select(o => 8))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_column_unary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID) + .Union(ss.Set().Select(o => -o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_column_binary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID) + .Union(ss.Set().Select(o => o.OrderID + 1))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_column_scalarsubquery(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID) + .Union(ss.Set().Select(o => o.OrderDetails.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_function_column(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()) + .Union(ss.Set().Select(o => o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_function_function(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()) + .Union(ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_function_constant(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()) + .Union(ss.Set().Select(o => 8))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_function_unary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()) + .Union(ss.Set().Select(o => -o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_function_binary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()) + .Union(ss.Set().Select(o => o.OrderID + 1))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_function_scalarsubquery(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()) + .Union(ss.Set().Select(o => o.OrderDetails.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_constant_column(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => 8) + .Union(ss.Set().Select(o => o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_constant_function(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => 8) + .Union(ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_constant_constant(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => 8) + .Union(ss.Set().Select(o => 8))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_constant_unary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => 8) + .Union(ss.Set().Select(o => -o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_constant_binary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => 8) + .Union(ss.Set().Select(o => o.OrderID + 1))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_constant_scalarsubquery(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => 8) + .Union(ss.Set().Select(o => o.OrderDetails.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_unary_column(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => -o.OrderID) + .Union(ss.Set().Select(o => o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_unary_function(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => -o.OrderID) + .Union(ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_unary_constant(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => -o.OrderID) + .Union(ss.Set().Select(o => 8))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_unary_unary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => -o.OrderID) + .Union(ss.Set().Select(o => -o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_unary_binary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => -o.OrderID) + .Union(ss.Set().Select(o => o.OrderID + 1))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_unary_scalarsubquery(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => -o.OrderID) + .Union(ss.Set().Select(o => o.OrderDetails.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_binary_column(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID + 1) + .Union(ss.Set().Select(o => o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_binary_function(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID + 1) + .Union(ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_binary_constant(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID + 1) + .Union(ss.Set().Select(o => 8))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_binary_unary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID + 1) + .Union(ss.Set().Select(o => -o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_binary_binary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID + 1) + .Union(ss.Set().Select(o => o.OrderID + 1))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_binary_scalarsubquery(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderID + 1) + .Union(ss.Set().Select(o => o.OrderDetails.Count()))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_scalarsubquery_column(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderDetails.Count()) + .Union(ss.Set().Select(o => o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_scalarsubquery_function(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderDetails.Count()) + .Union(ss.Set().GroupBy(o => o.OrderID).Select(g => g.Count()))); } - private static IEnumerable GetSetOperandTestCases() - => from async in new[] { true, false } - from leftType in _supportedOperandExpressionType - from rightType in _supportedOperandExpressionType - select new object[] { async, leftType, rightType }; + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_scalarsubquery_constant(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderDetails.Count()) + .Union(ss.Set().Select(o => 8))); + } - // ReSharper disable once StaticMemberInGenericType - private static readonly string[] _supportedOperandExpressionType = + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_scalarsubquery_unary(bool async) { - "Column", "Function", "Constant", "Unary", "Binary", "ScalarSubquery" - }; + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderDetails.Count()) + .Union(ss.Set().Select(o => -o.OrderID))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_scalarsubquery_binary(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderDetails.Count()) + .Union(ss.Set().Select(o => o.OrderID + 1))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Union_over_scalarsubquery_scalarsubquery(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(o => o.OrderDetails.Count()) + .Union(ss.Set().Select(o => o.OrderDetails.Count()))); + } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] diff --git a/test/EFCore.Specification.Tests/TestUtilities/QueryTestGeneration/ProceduralQueryExpressionGenerator.cs b/test/EFCore.Specification.Tests/TestUtilities/QueryTestGeneration/ProceduralQueryExpressionGenerator.cs index 95cba7b59fe..fe0cc13371e 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/QueryTestGeneration/ProceduralQueryExpressionGenerator.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/QueryTestGeneration/ProceduralQueryExpressionGenerator.cs @@ -173,10 +173,6 @@ static ProcedurallyGeneratedQueryExecutor() "Collection_select_nav_prop_first_or_default_then_nav_prop_nested_using_property_method", "Unable to cast object of type 'System.String' to type"); // 12601 - AddExpectedFailure("GroupBy_Shadow", "Value does not fall within the expected range."); // 12088 - AddExpectedFailure("GroupBy_Shadow3", "Value does not fall within the expected range."); // 12088 - AddExpectedFailure("GroupBy_SelectMany", "Value does not fall within the expected range."); // 12088 - AddExpectedFailure("GroupJoin_GroupBy_Aggregate_5", "Incorrect syntax near '+'."); // 12656 AddExpectedFailure("GroupBy_Key_as_part_of_element_selector", "Incorrect syntax near '+'."); // 12656 AddExpectedFailure("GroupBy_Property_Select_Key_Min", "Incorrect syntax near '+'."); // 12656 diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs index 2e36283e717..c6a41f5b5b5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsQuerySqlServerTest.cs @@ -698,6 +698,78 @@ public override async Task Include_collection_with_multiple_orderbys_complex_rep ORDER BY -[l].[Level1_Required_Id], [l].[Name], [l].[Id], [l0].[Id]"); } + public override async Task Include_collection_with_groupby_in_subquery(bool async) + { + await base.Include_collection_with_groupby_in_subquery(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t].[Name], [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +LEFT JOIN [LevelTwo] AS [l1] ON [t0].[Id] = [l1].[OneToMany_Optional_Inverse2Id] +ORDER BY [t].[Name], [t0].[Name], [l1].[Id]"); + } + + public override async Task Include_collection_with_groupby_in_subquery_and_filter_before_groupby(bool async) + { + await base.Include_collection_with_groupby_in_subquery_and_filter_before_groupby(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t].[Name], [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + WHERE [l].[Id] > 3 + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + WHERE [l0].[Id] > 3 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +LEFT JOIN [LevelTwo] AS [l1] ON [t0].[Id] = [l1].[OneToMany_Optional_Inverse2Id] +ORDER BY [t].[Name], [t0].[Name], [l1].[Id]"); + } + + public override async Task Include_collection_with_groupby_in_subquery_and_filter_after_groupby(bool async) + { + await base.Include_collection_with_groupby_in_subquery_and_filter_after_groupby(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t].[Name], [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] + HAVING ([l].[Name] <> N'Foo') OR [l].[Name] IS NULL +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +LEFT JOIN [LevelTwo] AS [l1] ON [t0].[Id] = [l1].[OneToMany_Optional_Inverse2Id] +ORDER BY [t].[Name], [t0].[Name], [l1].[Id]"); + } + public override async Task Include_reference_collection_order_by_reference_navigation(bool async) { await base.Include_reference_collection_order_by_reference_navigation(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs index 0cf51ca17bc..78f1814f64a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsCollectionsSplitQuerySqlServerTest.cs @@ -2151,21 +2151,123 @@ public override async Task Include_collection_with_groupby_in_subquery(bool asyn { await base.Include_collection_with_groupby_in_subquery(async); - AssertSql(" "); + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t].[Name] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +ORDER BY [t].[Name], [t0].[Name]", + // + @"SELECT [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id], [t].[Name], [t0].[Name] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Name] + FROM ( + SELECT [l0].[Id], [l0].[Name], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +INNER JOIN [LevelTwo] AS [l1] ON [t0].[Id] = [l1].[OneToMany_Optional_Inverse2Id] +ORDER BY [t].[Name], [t0].[Name]"); } public override async Task Include_collection_with_groupby_in_subquery_and_filter_before_groupby(bool async) { await base.Include_collection_with_groupby_in_subquery_and_filter_before_groupby(async); - AssertSql(" "); + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t].[Name] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + WHERE [l].[Id] > 3 + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + WHERE [l0].[Id] > 3 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +ORDER BY [t].[Name], [t0].[Name]", + // + @"SELECT [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id], [t].[Name], [t0].[Name] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + WHERE [l].[Id] > 3 + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Name] + FROM ( + SELECT [l0].[Id], [l0].[Name], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + WHERE [l0].[Id] > 3 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +INNER JOIN [LevelTwo] AS [l1] ON [t0].[Id] = [l1].[OneToMany_Optional_Inverse2Id] +ORDER BY [t].[Name], [t0].[Name]"); } public override async Task Include_collection_with_groupby_in_subquery_and_filter_after_groupby(bool async) { await base.Include_collection_with_groupby_in_subquery_and_filter_after_groupby(async); - AssertSql(" "); + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t].[Name] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] + HAVING ([l].[Name] <> N'Foo') OR [l].[Name] IS NULL +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +ORDER BY [t].[Name], [t0].[Name]", + // + @"SELECT [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id], [t].[Name], [t0].[Name] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] + HAVING ([l].[Name] <> N'Foo') OR [l].[Name] IS NULL +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Name] + FROM ( + SELECT [l0].[Id], [l0].[Name], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +INNER JOIN [LevelTwo] AS [l1] ON [t0].[Id] = [l1].[OneToMany_Optional_Inverse2Id] +ORDER BY [t].[Name], [t0].[Name]"); } public override async Task Include_reference_collection_order_by_reference_navigation(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 9ce9bf0d096..764effc5202 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -2559,6 +2559,52 @@ public override async Task Select_optional_navigation_property_string_concat(boo ) AS [t] ON [l].[Id] = [t].[OneToMany_Optional_Inverse2Id]"); } + public override async Task Include_reference_with_groupby_in_subquery(bool async) + { + await base.Include_reference_with_groupby_in_subquery(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t0].[Id0], [t0].[Date0], [t0].[Level1_Optional_Id], [t0].[Level1_Required_Id], [t0].[Name0], [t0].[OneToMany_Optional_Inverse2Id], [t0].[OneToMany_Optional_Self_Inverse2Id], [t0].[OneToMany_Required_Inverse2Id], [t0].[OneToMany_Required_Self_Inverse2Id], [t0].[OneToOne_Optional_PK_Inverse2Id], [t0].[OneToOne_Optional_Self2Id] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id], [t1].[Id0], [t1].[Date0], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], [t1].[Name0], [t1].[OneToMany_Optional_Inverse2Id], [t1].[OneToMany_Optional_Self_Inverse2Id], [t1].[OneToMany_Required_Inverse2Id], [t1].[OneToMany_Required_Self_Inverse2Id], [t1].[OneToOne_Optional_PK_Inverse2Id], [t1].[OneToOne_Optional_Self2Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], [l1].[Id] AS [Id0], [l1].[Date] AS [Date0], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name] AS [Name0], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + LEFT JOIN [LevelTwo] AS [l1] ON [l0].[Id] = [l1].[Level1_Optional_Id] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name]"); + } + + public override async Task Multi_include_with_groupby_in_subquery(bool async) + { + await base.Multi_include_with_groupby_in_subquery(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Date], [t0].[Name], [t0].[OneToMany_Optional_Self_Inverse1Id], [t0].[OneToMany_Required_Self_Inverse1Id], [t0].[OneToOne_Optional_Self1Id], [t0].[Id0], [t0].[Date0], [t0].[Level1_Optional_Id], [t0].[Level1_Required_Id], [t0].[Name0], [t0].[OneToMany_Optional_Inverse2Id], [t0].[OneToMany_Optional_Self_Inverse2Id], [t0].[OneToMany_Required_Inverse2Id], [t0].[OneToMany_Required_Self_Inverse2Id], [t0].[OneToOne_Optional_PK_Inverse2Id], [t0].[OneToOne_Optional_Self2Id], [t].[Name], [l2].[Id], [l2].[Level2_Optional_Id], [l2].[Level2_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_Inverse3Id], [l2].[OneToMany_Optional_Self_Inverse3Id], [l2].[OneToMany_Required_Inverse3Id], [l2].[OneToMany_Required_Self_Inverse3Id], [l2].[OneToOne_Optional_PK_Inverse3Id], [l2].[OneToOne_Optional_Self3Id] +FROM ( + SELECT [l].[Name] + FROM [LevelOne] AS [l] + GROUP BY [l].[Name] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Id], [t1].[Date], [t1].[Name], [t1].[OneToMany_Optional_Self_Inverse1Id], [t1].[OneToMany_Required_Self_Inverse1Id], [t1].[OneToOne_Optional_Self1Id], [t1].[Id0], [t1].[Date0], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], [t1].[Name0], [t1].[OneToMany_Optional_Inverse2Id], [t1].[OneToMany_Optional_Self_Inverse2Id], [t1].[OneToMany_Required_Inverse2Id], [t1].[OneToMany_Required_Self_Inverse2Id], [t1].[OneToOne_Optional_PK_Inverse2Id], [t1].[OneToOne_Optional_Self2Id] + FROM ( + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [l0].[OneToMany_Optional_Self_Inverse1Id], [l0].[OneToMany_Required_Self_Inverse1Id], [l0].[OneToOne_Optional_Self1Id], [l1].[Id] AS [Id0], [l1].[Date] AS [Date0], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name] AS [Name0], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id], ROW_NUMBER() OVER(PARTITION BY [l0].[Name] ORDER BY [l0].[Id]) AS [row] + FROM [LevelOne] AS [l0] + LEFT JOIN [LevelTwo] AS [l1] ON [l0].[Id] = [l1].[Level1_Optional_Id] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Name] = [t0].[Name] +LEFT JOIN [LevelThree] AS [l2] ON [t0].[Id0] = [l2].[OneToMany_Optional_Inverse3Id] +ORDER BY [t].[Name], [t0].[Name], [t0].[Id0], [l2].[Id]"); + } + public override async Task String_include_multiple_derived_navigation_with_same_name_and_same_type(bool async) { await base.String_include_multiple_derived_navigation_with_same_name_and_same_type(async); @@ -3407,8 +3453,9 @@ public override async Task Element_selector_with_coalesce_repeated_in_aggregate( FROM [LevelOne] AS [l] LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Id] LEFT JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[Id] +LEFT JOIN [LevelTwo] AS [l2] ON [l].[Id] = [l2].[Id] GROUP BY [l1].[Name] -HAVING MIN(COALESCE([l0].[Id], 0) + COALESCE([l0].[Id], 0)) > 0"); +HAVING MIN(COALESCE([l2].[Id], 0) + COALESCE([l2].[Id], 0)) > 0"); } public override async Task Nested_object_constructed_from_group_key_properties(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 2dc8a0b7972..0373cf59881 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -118,8 +118,14 @@ public override async Task Simple_level1_level2_GroupBy_Having_Count(bool async) ) AS [t1] ON [l2].[Id] = [t1].[Id] WHERE [l2].[Level2_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse3Id] IS NOT NULL ) AS [t0] ON [t].[Id] = [t0].[Id] +LEFT JOIN ( + SELECT [l5].[Id] + FROM [Level1] AS [l5] + INNER JOIN [Level1] AS [l6] ON [l5].[Id] = [l6].[Id] + WHERE ([l5].[OneToOne_Required_PK_Date] IS NOT NULL AND [l5].[Level1_Required_Id] IS NOT NULL) AND [l5].[OneToMany_Required_Inverse2Id] IS NOT NULL +) AS [t2] ON [l].[Id] = [t2].[Id] GROUP BY [t0].[Level3_Name] -HAVING MIN(COALESCE([t].[Id], 0)) > 0"); +HAVING MIN(COALESCE([t2].[Id], 0)) > 0"); } public override async Task Simple_level1_level2_level3_include(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index eb935223f31..c048d01aacd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -5564,11 +5564,22 @@ public override async Task Group_by_with_include_with_entity_in_result_selector( await base.Group_by_with_include_with_entity_in_result_selector(async); AssertSql( - @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [g.CityOfBirth].[Name], [g.CityOfBirth].[Location], [g.CityOfBirth].[Nation] -FROM [Gears] AS [g] -INNER JOIN [Cities] AS [g.CityOfBirth] ON [g].[CityOfBirthName] = [g.CityOfBirth].[Name] -WHERE [g].[Discriminator] IN (N'Officer', N'Gear') -ORDER BY [g].[Rank]"); + @"SELECT [t].[Rank], [t].[c], [t0].[Nickname], [t0].[SquadId], [t0].[AssignedCityName], [t0].[CityOfBirthName], [t0].[Discriminator], [t0].[FullName], [t0].[HasSoulPatch], [t0].[LeaderNickname], [t0].[LeaderSquadId], [t0].[Rank], [t0].[Name], [t0].[Location], [t0].[Nation] +FROM ( + SELECT [g].[Rank], COUNT(*) AS [c] + FROM [Gears] AS [g] + GROUP BY [g].[Rank] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Nickname], [t1].[SquadId], [t1].[AssignedCityName], [t1].[CityOfBirthName], [t1].[Discriminator], [t1].[FullName], [t1].[HasSoulPatch], [t1].[LeaderNickname], [t1].[LeaderSquadId], [t1].[Rank], [t1].[Name], [t1].[Location], [t1].[Nation] + FROM ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [c].[Name], [c].[Location], [c].[Nation], ROW_NUMBER() OVER(PARTITION BY [g0].[Rank] ORDER BY [g0].[Nickname]) AS [row] + FROM [Gears] AS [g0] + INNER JOIN [Cities] AS [c] ON [g0].[CityOfBirthName] = [c].[Name] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Rank] = [t0].[Rank] +ORDER BY [t].[Rank]"); } public override async Task GroupBy_Property_Include_Select_Max(bool async) @@ -5586,10 +5597,22 @@ public override async Task Include_with_group_by_and_FirstOrDefault_gets_properl await base.Include_with_group_by_and_FirstOrDefault_gets_properly_applied(async); AssertSql( - @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [g.CityOfBirth].[Name], [g.CityOfBirth].[Location], [g.CityOfBirth].[Nation] -FROM [Gears] AS [g] -INNER JOIN [Cities] AS [g.CityOfBirth] ON [g].[CityOfBirthName] = [g.CityOfBirth].[Name] -ORDER BY [g].[Rank]"); + @"SELECT [t0].[Nickname], [t0].[SquadId], [t0].[AssignedCityName], [t0].[CityOfBirthName], [t0].[Discriminator], [t0].[FullName], [t0].[HasSoulPatch], [t0].[LeaderNickname], [t0].[LeaderSquadId], [t0].[Rank], [t0].[Name], [t0].[Location], [t0].[Nation] +FROM ( + SELECT [g].[Rank] + FROM [Gears] AS [g] + GROUP BY [g].[Rank] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Nickname], [t1].[SquadId], [t1].[AssignedCityName], [t1].[CityOfBirthName], [t1].[Discriminator], [t1].[FullName], [t1].[HasSoulPatch], [t1].[LeaderNickname], [t1].[LeaderSquadId], [t1].[Rank], [t1].[Name], [t1].[Location], [t1].[Nation] + FROM ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [c].[Name], [c].[Location], [c].[Nation], ROW_NUMBER() OVER(PARTITION BY [g0].[Rank] ORDER BY [g0].[Rank], [c].[Name]) AS [row] + FROM [Gears] AS [g0] + INNER JOIN [Cities] AS [c] ON [g0].[CityOfBirthName] = [c].[Name] + WHERE [g0].[HasSoulPatch] = CAST(1 AS bit) + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Rank] = [t0].[Rank]"); } public override async Task Include_collection_with_Cast_to_base(bool async) @@ -6383,15 +6406,15 @@ public override async Task Complex_GroupBy_after_set_operator(bool async) FROM ( SELECT [c].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w0] - WHERE [g].[FullName] = [w0].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName]) AS [Count] FROM [Gears] AS [g] LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name] UNION ALL SELECT [c0].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w] - WHERE [g0].[FullName] = [w].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w0] + WHERE [g0].[FullName] = [w0].[OwnerFullName]) AS [Count] FROM [Gears] AS [g0] INNER JOIN [Cities] AS [c0] ON [g0].[CityOfBirthName] = [c0].[Name] ) AS [t] @@ -6407,15 +6430,15 @@ public override async Task Complex_GroupBy_after_set_operator_using_result_selec FROM ( SELECT [c].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w0] - WHERE [g].[FullName] = [w0].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName]) AS [Count] FROM [Gears] AS [g] LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name] UNION ALL SELECT [c0].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w] - WHERE [g0].[FullName] = [w].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w0] + WHERE [g0].[FullName] = [w0].[OwnerFullName]) AS [Count] FROM [Gears] AS [g0] INNER JOIN [Cities] AS [c0] ON [g0].[CityOfBirthName] = [c0].[Name] ) AS [t] @@ -6470,8 +6493,6 @@ public override async Task Group_by_over_projection_with_multiple_properties_acc @"SELECT [c].[Name] FROM [Gears] AS [g] INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name] -LEFT JOIN [Cities] AS [c0] ON [g].[AssignedCityName] = [c0].[Name] -INNER JOIN [Squads] AS [s] ON [g].[SquadId] = [s].[Id] GROUP BY [c].[Name]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 61383e433b4..470313d494e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -722,7 +722,7 @@ public override async Task GroupBy_Property_scalar_element_selector_Count(bool a await base.GroupBy_Property_scalar_element_selector_Count(async); AssertSql( - @"SELECT COUNT([o].[OrderID]) + @"SELECT COUNT(*) FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); } @@ -732,7 +732,7 @@ public override async Task GroupBy_Property_scalar_element_selector_LongCount(bo await base.GroupBy_Property_scalar_element_selector_LongCount(async); AssertSql( - @"SELECT COUNT_BIG([o].[OrderID]) + @"SELECT COUNT_BIG(*) FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); } @@ -911,8 +911,12 @@ public override async Task GroupBy_empty_key_Aggregate(bool async) await base.GroupBy_empty_key_Aggregate(async); AssertSql( - @"SELECT COALESCE(SUM([o].[OrderID]), 0) -FROM [Orders] AS [o]"); + @"SELECT COALESCE(SUM([t].[OrderID]), 0) +FROM ( + SELECT [o].[OrderID], 1 AS [Key] + FROM [Orders] AS [o] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task GroupBy_empty_key_Aggregate_Key(bool async) @@ -920,8 +924,12 @@ public override async Task GroupBy_empty_key_Aggregate_Key(bool async) await base.GroupBy_empty_key_Aggregate_Key(async); AssertSql( - @"SELECT COALESCE(SUM([o].[OrderID]), 0) AS [Sum] -FROM [Orders] AS [o]"); + @"SELECT COALESCE(SUM([t].[OrderID]), 0) AS [Sum] +FROM ( + SELECT [o].[OrderID], 1 AS [Key] + FROM [Orders] AS [o] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task OrderBy_GroupBy_Aggregate(bool async) @@ -1545,45 +1553,22 @@ public override async Task Select_nested_collection_with_groupby(bool async) await base.Select_nested_collection_with_groupby(async); AssertSql( - @"SELECT ( - SELECT CASE - WHEN EXISTS ( - SELECT 1 - FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID]) - THEN CAST(1 AS bit) ELSE CAST(0 AS bit) - END -), [c].[CustomerID] + @"SELECT CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [c].[CustomerID], [t].[OrderID] FROM [Customers] AS [c] -WHERE [c].[CustomerID] LIKE N'A%'", - // - @"@_outer_CustomerID='ALFKI' (Size = 5) - -SELECT [o1].[OrderID] -FROM [Orders] AS [o1] -WHERE @_outer_CustomerID = [o1].[CustomerID] -ORDER BY [o1].[OrderID]", - // - @"@_outer_CustomerID='ANATR' (Size = 5) - -SELECT [o1].[OrderID] -FROM [Orders] AS [o1] -WHERE @_outer_CustomerID = [o1].[CustomerID] -ORDER BY [o1].[OrderID]", - // - @"@_outer_CustomerID='ANTON' (Size = 5) - -SELECT [o1].[OrderID] -FROM [Orders] AS [o1] -WHERE @_outer_CustomerID = [o1].[CustomerID] -ORDER BY [o1].[OrderID]", - // - @"@_outer_CustomerID='AROUT' (Size = 5) - -SELECT [o1].[OrderID] -FROM [Orders] AS [o1] -WHERE @_outer_CustomerID = [o1].[CustomerID] -ORDER BY [o1].[OrderID]"); +OUTER APPLY ( + SELECT [o0].[OrderID] + FROM [Orders] AS [o0] + WHERE [c].[CustomerID] = [o0].[CustomerID] + GROUP BY [o0].[OrderID] +) AS [t] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [t].[OrderID]"); } public override async Task Select_uncorrelated_collection_with_groupby_works(bool async) @@ -1716,7 +1701,7 @@ public override async Task GroupBy_Where_Count_with_predicate(bool async) AssertSql( @"SELECT COUNT(CASE - WHEN ([o].[OrderID] < 10300) AND ([o].[OrderDate] IS NOT NULL AND (DATEPART(year, [o].[OrderDate]) = 1997)) THEN 1 + WHEN (([o].[OrderID] < 10300) AND [o].[OrderDate] IS NOT NULL) AND (DATEPART(year, [o].[OrderDate]) = 1997) THEN 1 END) FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); @@ -1728,7 +1713,7 @@ public override async Task GroupBy_Where_Where_Count(bool async) AssertSql( @"SELECT COUNT(CASE - WHEN ([o].[OrderID] < 10300) AND ([o].[OrderDate] IS NOT NULL AND (DATEPART(year, [o].[OrderDate]) = 1997)) THEN 1 + WHEN (([o].[OrderID] < 10300) AND [o].[OrderDate] IS NOT NULL) AND (DATEPART(year, [o].[OrderDate]) = 1997) THEN 1 END) FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); @@ -1740,7 +1725,7 @@ public override async Task GroupBy_Where_Select_Where_Count(bool async) AssertSql( @"SELECT COUNT(CASE - WHEN ([o].[OrderID] < 10300) AND ([o].[OrderDate] IS NOT NULL AND (DATEPART(year, [o].[OrderDate]) = 1997)) THEN [o].[OrderDate] + WHEN (([o].[OrderID] < 10300) AND [o].[OrderDate] IS NOT NULL) AND (DATEPART(year, [o].[OrderDate]) = 1997) THEN 1 END) FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); @@ -1752,7 +1737,7 @@ public override async Task GroupBy_Where_Select_Where_Select_Min(bool async) AssertSql( @"SELECT MIN(CASE - WHEN ([o].[OrderID] < 10300) AND ([o].[OrderDate] IS NOT NULL AND (DATEPART(year, [o].[OrderDate]) = 1997)) THEN [o].[OrderID] + WHEN (([o].[OrderID] < 10300) AND [o].[OrderDate] IS NOT NULL) AND (DATEPART(year, [o].[OrderDate]) = 1997) THEN [o].[OrderID] END) FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); @@ -1824,68 +1809,15 @@ public override async Task GroupBy_composite_Key_as_part_of_element_selector(boo GROUP BY [o].[OrderID], [o].[CustomerID]"); } - public override async Task GroupBy_SelectMany(bool async) - { - await base.GroupBy_SelectMany(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] -FROM [Customers] AS [c] -ORDER BY [c].[City]"); - } - - public override async Task OrderBy_GroupBy_SelectMany(bool async) - { - await base.OrderBy_GroupBy_SelectMany(async); - - AssertSql( - @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] -FROM [Orders] AS [o] -ORDER BY [o].[CustomerID], [o].[OrderID]"); - } - - public override async Task OrderBy_GroupBy_SelectMany_shadow(bool async) - { - await base.OrderBy_GroupBy_SelectMany_shadow(async); - - AssertSql( - @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] -FROM [Employees] AS [e] -ORDER BY [e].[EmployeeID]"); - } - - public override async Task GroupBy_with_orderby_take_skip_distinct_followed_by_group_key_projection(bool async) - { - await base.GroupBy_with_orderby_take_skip_distinct_followed_by_group_key_projection(async); - - AssertSql( - ""); - } - - public override async Task GroupBy_Distinct(bool async) - { - await base.GroupBy_Distinct(async); - - AssertSql( - @"SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] -FROM [Orders] AS [o0] -ORDER BY [o0].[CustomerID]"); - } - public override async Task GroupBy_with_aggregate_through_navigation_property(bool async) { await base.GroupBy_with_aggregate_through_navigation_property(async); AssertSql( - @"SELECT [c].[OrderID], [c].[CustomerID], [c].[EmployeeID], [c].[OrderDate] -FROM [Orders] AS [c] -ORDER BY [c].[EmployeeID]", - // - @"SELECT [i.Customer0].[CustomerID], [i.Customer0].[Region] -FROM [Customers] AS [i.Customer0]", - // - @"SELECT [i.Customer0].[CustomerID], [i.Customer0].[Region] -FROM [Customers] AS [i.Customer0]"); + @"SELECT MAX([c].[Region]) AS [max] +FROM [Orders] AS [o] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +GROUP BY [o].[EmployeeID]"); } public override async Task GroupBy_Shadow(bool async) @@ -1893,10 +1825,13 @@ public override async Task GroupBy_Shadow(bool async) await base.GroupBy_Shadow(async); AssertSql( - @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] + @"SELECT ( + SELECT TOP(1) [e0].[Title] + FROM [Employees] AS [e0] + WHERE (([e0].[Title] = N'Sales Representative') AND ([e0].[EmployeeID] = 1)) AND (([e].[Title] = [e0].[Title]) OR ([e].[Title] IS NULL AND [e0].[Title] IS NULL))) FROM [Employees] AS [e] WHERE ([e].[Title] = N'Sales Representative') AND ([e].[EmployeeID] = 1) -ORDER BY [e].[Title]"); +GROUP BY [e].[Title]"); } public override async Task GroupBy_Shadow2(bool async) @@ -1904,10 +1839,22 @@ public override async Task GroupBy_Shadow2(bool async) await base.GroupBy_Shadow2(async); AssertSql( - @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] -FROM [Employees] AS [e] -WHERE ([e].[Title] = N'Sales Representative') AND ([e].[EmployeeID] = 1) -ORDER BY [e].[Title]"); + @"SELECT [t0].[EmployeeID], [t0].[City], [t0].[Country], [t0].[FirstName], [t0].[ReportsTo], [t0].[Title] +FROM ( + SELECT [e].[Title] + FROM [Employees] AS [e] + WHERE ([e].[Title] = N'Sales Representative') AND ([e].[EmployeeID] = 1) + GROUP BY [e].[Title] +) AS [t] +LEFT JOIN ( + SELECT [t1].[EmployeeID], [t1].[City], [t1].[Country], [t1].[FirstName], [t1].[ReportsTo], [t1].[Title] + FROM ( + SELECT [e0].[EmployeeID], [e0].[City], [e0].[Country], [e0].[FirstName], [e0].[ReportsTo], [e0].[Title], ROW_NUMBER() OVER(PARTITION BY [e0].[Title] ORDER BY [e0].[Title]) AS [row] + FROM [Employees] AS [e0] + WHERE ([e0].[Title] = N'Sales Representative') AND ([e0].[EmployeeID] = 1) + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Title] = [t0].[Title]"); } public override async Task GroupBy_Shadow3(bool async) @@ -1915,10 +1862,13 @@ public override async Task GroupBy_Shadow3(bool async) await base.GroupBy_Shadow3(async); AssertSql( - @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] + @"SELECT ( + SELECT TOP(1) [e0].[Title] + FROM [Employees] AS [e0] + WHERE ([e0].[EmployeeID] = 1) AND ([e].[EmployeeID] = [e0].[EmployeeID])) FROM [Employees] AS [e] WHERE [e].[EmployeeID] = 1 -ORDER BY [e].[EmployeeID]"); +GROUP BY [e].[EmployeeID]"); } public override async Task Select_GroupBy_SelectMany(bool async) @@ -1972,7 +1922,13 @@ public override async Task GroupBy_group_Distinct_Select_Distinct_aggregate(bool await base.GroupBy_group_Distinct_Select_Distinct_aggregate(async); AssertSql( - @"SELECT [o].[CustomerID] AS [Key], MAX(DISTINCT ([o].[OrderDate])) AS [Max] + @"SELECT [o].[CustomerID] AS [Key], ( + SELECT DISTINCT MAX(DISTINCT ([t].[OrderDate])) + FROM ( + SELECT DISTINCT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE ([o].[CustomerID] = [o0].[CustomerID]) OR ([o].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL) + ) AS [t]) AS [Max] FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); } @@ -2500,7 +2456,15 @@ public override async Task Complex_query_with_groupBy_in_subquery1(bool async) await base.Complex_query_with_groupBy_in_subquery1(async); AssertSql( - @""); + @"SELECT [c].[CustomerID], [t].[Sum], [t].[CustomerID] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT COALESCE(SUM([o].[OrderID]), 0) AS [Sum], [o].[CustomerID] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + GROUP BY [o].[CustomerID] +) AS [t] +ORDER BY [c].[CustomerID], [t].[CustomerID]"); } public override async Task Complex_query_with_groupBy_in_subquery2(bool async) @@ -2508,7 +2472,15 @@ public override async Task Complex_query_with_groupBy_in_subquery2(bool async) await base.Complex_query_with_groupBy_in_subquery2(async); AssertSql( - @""); + @"SELECT [c].[CustomerID], [t].[Max], [t].[Sum], [t].[CustomerID] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT MAX(CAST(LEN([o].[CustomerID]) AS int)) AS [Max], COALESCE(SUM([o].[OrderID]), 0) AS [Sum], [o].[CustomerID] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] + GROUP BY [o].[CustomerID] +) AS [t] +ORDER BY [c].[CustomerID], [t].[CustomerID]"); } public override async Task Complex_query_with_groupBy_in_subquery3(bool async) @@ -2516,7 +2488,14 @@ public override async Task Complex_query_with_groupBy_in_subquery3(bool async) await base.Complex_query_with_groupBy_in_subquery3(async); AssertSql( - @""); + @"SELECT [c].[CustomerID], [t].[Max], [t].[Sum], [t].[CustomerID] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT MAX(CAST(LEN([o].[CustomerID]) AS int)) AS [Max], COALESCE(SUM([o].[OrderID]), 0) AS [Sum], [o].[CustomerID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] +) AS [t] +ORDER BY [c].[CustomerID], [t].[CustomerID]"); } public override async Task Group_by_with_projection_into_DTO(bool async) @@ -2594,6 +2573,35 @@ public override async Task GroupBy_scalar_subquery(bool async) GROUP BY [t].[Key]"); } + public override async Task AsEnumerable_in_subquery_for_GroupBy(bool async) + { + await base.AsEnumerable_in_subquery_for_GroupBy(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], [t2].[OrderID], [t2].[CustomerID], [t2].[EmployeeID], [t2].[OrderDate], [t2].[CustomerID0] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[CustomerID] AS [CustomerID0] + FROM ( + SELECT [o].[CustomerID] + FROM [Orders] AS [o] + WHERE [o].[CustomerID] = [c].[CustomerID] + GROUP BY [o].[CustomerID] + ) AS [t] + LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o0].[CustomerID] ORDER BY [o0].[OrderDate] DESC) AS [row] + FROM [Orders] AS [o0] + WHERE [o0].[CustomerID] = [c].[CustomerID] + ) AS [t1] + WHERE [t1].[row] <= 1 + ) AS [t0] ON [t].[CustomerID] = [t0].[CustomerID] +) AS [t2] +WHERE [c].[CustomerID] LIKE N'F%' +ORDER BY [c].[CustomerID], [t2].[CustomerID0], [t2].[CustomerID]"); + } + public override async Task GroupBy_scalar_aggregate_in_set_operation(bool async) { await base.GroupBy_scalar_aggregate_in_set_operation(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs index 4795dddf519..fbfe36215f8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindIncludeQuerySqlServerTest.cs @@ -1043,6 +1043,265 @@ ELSE 2 END, [c].[CustomerID], [o].[OrderID]"); } + public override async Task Include_collection_GroupBy_Select(bool async) + { + await base.Include_collection_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID], [o1].[OrderID], [o1].[ProductID], [o1].[Discount], [o1].[Quantity], [o1].[UnitPrice] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o0].[OrderID] ORDER BY [o0].[OrderID]) AS [row] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +LEFT JOIN [Order Details] AS [o1] ON [t0].[OrderID] = [o1].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID], [o1].[OrderID], [o1].[ProductID]"); + } + + public override async Task Include_reference_GroupBy_Select(bool async) + { + await base.Include_reference_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[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], ROW_NUMBER() OVER(PARTITION BY [o0].[OrderID] ORDER BY [o0].[OrderID]) AS [row] + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE [o0].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task Include_collection_Join_GroupBy_Select(bool async) + { + await base.Include_collection_Join_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID], [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + INNER JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + INNER JOIN [Order Details] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +LEFT JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID], [o3].[OrderID], [o3].[ProductID]"); + } + + public override async Task Include_reference_Join_GroupBy_Select(bool async) + { + await base.Include_reference_Join_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + INNER JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[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], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + INNER JOIN [Order Details] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + LEFT JOIN [Customers] AS [c] ON [o1].[CustomerID] = [c].[CustomerID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task Join_Include_collection_GroupBy_Select(bool async) + { + await base.Join_Include_collection_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID], [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + INNER JOIN [Orders] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +LEFT JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID], [o3].[OrderID], [o3].[ProductID]"); + } + + public override async Task Join_Include_reference_GroupBy_Select(bool async) + { + await base.Join_Include_reference_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[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], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + INNER JOIN [Orders] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + LEFT JOIN [Customers] AS [c] ON [o2].[CustomerID] = [c].[CustomerID] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task Include_collection_SelectMany_GroupBy_Select(bool async) + { + await base.Include_collection_SelectMany_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID], [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + CROSS JOIN [Order Details] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + CROSS JOIN [Order Details] AS [o2] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +LEFT JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID], [o3].[OrderID], [o3].[ProductID]"); + } + + public override async Task Include_reference_SelectMany_GroupBy_Select(bool async) + { + await base.Include_reference_SelectMany_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + CROSS JOIN [Order Details] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[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], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + CROSS JOIN [Order Details] AS [o2] + LEFT JOIN [Customers] AS [c] ON [o1].[CustomerID] = [c].[CustomerID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task SelectMany_Include_collection_GroupBy_Select(bool async) + { + await base.SelectMany_Include_collection_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID], [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + CROSS JOIN [Orders] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + CROSS JOIN [Orders] AS [o2] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +LEFT JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID], [o3].[OrderID], [o3].[ProductID]"); + } + + public override async Task SelectMany_Include_reference_GroupBy_Select(bool async) + { + await base.SelectMany_Include_reference_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + CROSS JOIN [Orders] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[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], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + CROSS JOIN [Orders] AS [o2] + LEFT JOIN [Customers] AS [c] ON [o2].[CustomerID] = [c].[CustomerID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + public override async Task Include_reference_distinct_is_server_evaluated(bool async) { await base.Include_reference_distinct_is_server_evaluated(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs index 0db50d146cb..33fbd8bd7c2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSetOperationsQuerySqlServerTest.cs @@ -386,14 +386,14 @@ public override async Task SubSelect_Union(bool 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], ( SELECT COUNT(*) - FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID]) AS [Orders] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]) AS [Orders] FROM [Customers] AS [c] UNION SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], ( SELECT COUNT(*) - FROM [Orders] AS [o] - WHERE [c0].[CustomerID] = [o].[CustomerID]) AS [Orders] + FROM [Orders] AS [o0] + WHERE [c0].[CustomerID] = [o0].[CustomerID]) AS [Orders] FROM [Customers] AS [c0]"); } @@ -425,99 +425,484 @@ UNION ALL FROM [Customers] AS [c0]"); } - public override async Task Union_over_different_projection_types(bool async, string leftType, string rightType) - { - await base.Union_over_different_projection_types(async, leftType, rightType); - - var leftSql = GenerateSql(leftType); - var rightSql = GenerateSql(rightType); - - switch (leftType) - { - case "Column": - leftSql = leftSql.Replace("{Alias}", ""); - break; - - case "Binary": - case "Constant": - case "Function": - case "ScalarSubquery": - case "Unary": - leftSql = leftSql.Replace("{Alias}", " AS [c]"); - break; - - default: - throw new ArgumentException("Unexpected type: " + leftType); - } - - switch (rightType) - { - case "Column": - rightSql = rightSql.Replace("{Alias}", leftType == "Column" ? "" : " AS [c]"); - break; - - case "Binary": - case "Constant": - case "Function": - case "ScalarSubquery": - case "Unary": - rightSql = rightSql.Replace("{Alias}", leftType == "Column" ? " AS [OrderID]" : " AS [c]"); - break; - default: - throw new ArgumentException("Unexpected type: " + rightType); - } - - // Fix up right-side SQL as table aliases shift - if (leftType == "ScalarSubquery") - { - if (rightType == "ScalarSubquery") - { - leftSql = leftSql.Replace("[o0]", "[o2]"); - rightSql = rightSql.Replace("[o0]", "[o1]").Replace("[o]", "[o0]"); - } - else - { - leftSql = leftSql.Replace("[o0]", "[o1]"); - rightSql = rightSql.Replace("[o]", "[o0]"); - } - } - else - { - rightSql = rightSql.Replace("[o0]", "[o1]").Replace("[o]", "[o0]"); - } - - AssertSql(leftSql + Environment.NewLine + "UNION" + Environment.NewLine + rightSql); - - static string GenerateSql(string expressionType) - { - switch (expressionType) - { - case "Column": - return @"SELECT [o].[OrderID]{Alias} -FROM [Orders] AS [o]"; - case "Function": - return @"SELECT COUNT(*){Alias} -FROM [Orders] AS [o] -GROUP BY [o].[OrderID]"; - case "Constant": - return @"SELECT 8{Alias} -FROM [Orders] AS [o]"; - case "Unary": - return @"SELECT -[o].[OrderID]{Alias} -FROM [Orders] AS [o]"; - case "Binary": - return @"SELECT [o].[OrderID] + 1{Alias} -FROM [Orders] AS [o]"; - case "ScalarSubquery": - return @"SELECT ( + public override async Task Union_over_column_column(bool async) + { + await base.Union_over_column_column(async); + + AssertSql( + @"SELECT [o].[OrderID] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_column_function(bool async) + { + await base.Union_over_column_function(async); + + AssertSql( + @"SELECT [o].[OrderID] +FROM [Orders] AS [o] +UNION +SELECT COUNT(*) AS [OrderID] +FROM [Orders] AS [o0] +GROUP BY [o0].[OrderID]"); + } + + public override async Task Union_over_column_constant(bool async) + { + await base.Union_over_column_constant(async); + + AssertSql( + @"SELECT [o].[OrderID] +FROM [Orders] AS [o] +UNION +SELECT 8 AS [OrderID] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_column_unary(bool async) + { + await base.Union_over_column_unary(async); + + AssertSql( + @"SELECT [o].[OrderID] +FROM [Orders] AS [o] +UNION +SELECT -[o0].[OrderID] AS [OrderID] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_column_binary(bool async) + { + await base.Union_over_column_binary(async); + + AssertSql( + @"SELECT [o].[OrderID] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] + 1 AS [OrderID] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_column_scalarsubquery(bool async) + { + await base.Union_over_column_scalarsubquery(async); + + AssertSql( + @"SELECT [o].[OrderID] +FROM [Orders] AS [o] +UNION +SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o1] + WHERE [o0].[OrderID] = [o1].[OrderID]) AS [OrderID] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_function_column(bool async) + { + await base.Union_over_function_column(async); + + AssertSql( + @"SELECT COUNT(*) AS [c] +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +UNION +SELECT [o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_function_function(bool async) + { + await base.Union_over_function_function(async); + + AssertSql( + @"SELECT COUNT(*) AS [c] +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +UNION +SELECT COUNT(*) AS [c] +FROM [Orders] AS [o0] +GROUP BY [o0].[OrderID]"); + } + + public override async Task Union_over_function_constant(bool async) + { + await base.Union_over_function_constant(async); + + AssertSql( + @"SELECT COUNT(*) AS [c] +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +UNION +SELECT 8 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_function_unary(bool async) + { + await base.Union_over_function_unary(async); + + AssertSql( + @"SELECT COUNT(*) AS [c] +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +UNION +SELECT -[o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_function_binary(bool async) + { + await base.Union_over_function_binary(async); + + AssertSql( + @"SELECT COUNT(*) AS [c] +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +UNION +SELECT [o0].[OrderID] + 1 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_function_scalarsubquery(bool async) + { + await base.Union_over_function_scalarsubquery(async); + + AssertSql( + @"SELECT COUNT(*) AS [c] +FROM [Orders] AS [o] +GROUP BY [o].[OrderID] +UNION +SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o1] + WHERE [o0].[OrderID] = [o1].[OrderID]) AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_constant_column(bool async) + { + await base.Union_over_constant_column(async); + + AssertSql( + @"SELECT 8 AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_constant_function(bool async) + { + await base.Union_over_constant_function(async); + + AssertSql( + @"SELECT 8 AS [c] +FROM [Orders] AS [o] +UNION +SELECT COUNT(*) AS [c] +FROM [Orders] AS [o0] +GROUP BY [o0].[OrderID]"); + } + + public override async Task Union_over_constant_constant(bool async) + { + await base.Union_over_constant_constant(async); + + AssertSql( + @"SELECT 8 AS [c] +FROM [Orders] AS [o] +UNION +SELECT 8 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_constant_unary(bool async) + { + await base.Union_over_constant_unary(async); + + AssertSql( + @"SELECT 8 AS [c] +FROM [Orders] AS [o] +UNION +SELECT -[o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_constant_binary(bool async) + { + await base.Union_over_constant_binary(async); + + AssertSql( + @"SELECT 8 AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] + 1 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_constant_scalarsubquery(bool async) + { + await base.Union_over_constant_scalarsubquery(async); + + AssertSql( + @"SELECT 8 AS [c] +FROM [Orders] AS [o] +UNION +SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o1] + WHERE [o0].[OrderID] = [o1].[OrderID]) AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_unary_column(bool async) + { + await base.Union_over_unary_column(async); + + AssertSql( + @"SELECT -[o].[OrderID] AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_unary_function(bool async) + { + await base.Union_over_unary_function(async); + + AssertSql( + @"SELECT -[o].[OrderID] AS [c] +FROM [Orders] AS [o] +UNION +SELECT COUNT(*) AS [c] +FROM [Orders] AS [o0] +GROUP BY [o0].[OrderID]"); + } + + public override async Task Union_over_unary_constant(bool async) + { + await base.Union_over_unary_constant(async); + + AssertSql( + @"SELECT -[o].[OrderID] AS [c] +FROM [Orders] AS [o] +UNION +SELECT 8 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_unary_unary(bool async) + { + await base.Union_over_unary_unary(async); + + AssertSql( + @"SELECT -[o].[OrderID] AS [c] +FROM [Orders] AS [o] +UNION +SELECT -[o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_unary_binary(bool async) + { + await base.Union_over_unary_binary(async); + + AssertSql( + @"SELECT -[o].[OrderID] AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] + 1 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_unary_scalarsubquery(bool async) + { + await base.Union_over_unary_scalarsubquery(async); + + AssertSql( + @"SELECT -[o].[OrderID] AS [c] +FROM [Orders] AS [o] +UNION +SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o1] + WHERE [o0].[OrderID] = [o1].[OrderID]) AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_binary_column(bool async) + { + await base.Union_over_binary_column(async); + + AssertSql( + @"SELECT [o].[OrderID] + 1 AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_binary_function(bool async) + { + await base.Union_over_binary_function(async); + + AssertSql( + @"SELECT [o].[OrderID] + 1 AS [c] +FROM [Orders] AS [o] +UNION +SELECT COUNT(*) AS [c] +FROM [Orders] AS [o0] +GROUP BY [o0].[OrderID]"); + } + + public override async Task Union_over_binary_constant(bool async) + { + await base.Union_over_binary_constant(async); + + AssertSql( + @"SELECT [o].[OrderID] + 1 AS [c] +FROM [Orders] AS [o] +UNION +SELECT 8 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_binary_unary(bool async) + { + await base.Union_over_binary_unary(async); + + AssertSql( + @"SELECT [o].[OrderID] + 1 AS [c] +FROM [Orders] AS [o] +UNION +SELECT -[o0].[OrderID] AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_binary_binary(bool async) + { + await base.Union_over_binary_binary(async); + + AssertSql( + @"SELECT [o].[OrderID] + 1 AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o0].[OrderID] + 1 AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_binary_scalarsubquery(bool async) + { + await base.Union_over_binary_scalarsubquery(async); + + AssertSql( + @"SELECT [o].[OrderID] + 1 AS [c] +FROM [Orders] AS [o] +UNION +SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o1] + WHERE [o0].[OrderID] = [o1].[OrderID]) AS [c] +FROM [Orders] AS [o0]"); + } + + public override async Task Union_over_scalarsubquery_column(bool async) + { + await base.Union_over_scalarsubquery_column(async); + + AssertSql( + @"SELECT ( SELECT COUNT(*) FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID]){Alias} -FROM [Orders] AS [o]"; - default: - throw new ArgumentException("Unexpected type: " + expressionType); - } - } + WHERE [o].[OrderID] = [o0].[OrderID]) AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o1].[OrderID] AS [c] +FROM [Orders] AS [o1]"); + } + + public override async Task Union_over_scalarsubquery_function(bool async) + { + await base.Union_over_scalarsubquery_function(async); + + AssertSql( + @"SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [c] +FROM [Orders] AS [o] +UNION +SELECT COUNT(*) AS [c] +FROM [Orders] AS [o1] +GROUP BY [o1].[OrderID]"); + } + + public override async Task Union_over_scalarsubquery_constant(bool async) + { + await base.Union_over_scalarsubquery_constant(async); + + AssertSql( + @"SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [c] +FROM [Orders] AS [o] +UNION +SELECT 8 AS [c] +FROM [Orders] AS [o1]"); + } + + public override async Task Union_over_scalarsubquery_unary(bool async) + { + await base.Union_over_scalarsubquery_unary(async); + + AssertSql( + @"SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [c] +FROM [Orders] AS [o] +UNION +SELECT -[o1].[OrderID] AS [c] +FROM [Orders] AS [o1]"); + } + + public override async Task Union_over_scalarsubquery_binary(bool async) + { + await base.Union_over_scalarsubquery_binary(async); + + AssertSql( + @"SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [c] +FROM [Orders] AS [o] +UNION +SELECT [o1].[OrderID] + 1 AS [c] +FROM [Orders] AS [o1]"); + } + + public override async Task Union_over_scalarsubquery_scalarsubquery(bool async) + { + await base.Union_over_scalarsubquery_scalarsubquery(async); + + AssertSql( + @"SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [c] +FROM [Orders] AS [o] +UNION +SELECT ( + SELECT COUNT(*) + FROM [Order Details] AS [o2] + WHERE [o1].[OrderID] = [o2].[OrderID]) AS [c] +FROM [Orders] AS [o1]"); } public override async Task OrderBy_Take_Union(bool async) @@ -534,12 +919,12 @@ public override async Task OrderBy_Take_Union(bool async) ORDER BY [c].[ContactName] ) AS [t] UNION -SELECT [t0].[CustomerID], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +SELECT [t1].[CustomerID], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] FROM ( SELECT TOP(@__p_0) [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].[ContactName] -) AS [t0]"); +) AS [t1]"); } public override async Task Collection_projection_after_set_operation(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs index 96cac71538c..15f238a0fc6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSplitIncludeQuerySqlServerTest.cs @@ -1405,6 +1405,363 @@ ELSE 2 END, [c].[CustomerID]"); } + public override async Task Include_collection_GroupBy_Select(bool async) + { + await base.Include_collection_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o0].[OrderID] ORDER BY [o0].[OrderID]) AS [row] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]", + // + @"SELECT [o1].[OrderID], [o1].[ProductID], [o1].[Discount], [o1].[Quantity], [o1].[UnitPrice], [t].[OrderID], [t0].[OrderID] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID] + FROM ( + SELECT [o0].[OrderID], ROW_NUMBER() OVER(PARTITION BY [o0].[OrderID] ORDER BY [o0].[OrderID]) AS [row] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +INNER JOIN [Order Details] AS [o1] ON [t0].[OrderID] = [o1].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]"); + } + + public override async Task Include_reference_GroupBy_Select(bool async) + { + await base.Include_reference_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[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], ROW_NUMBER() OVER(PARTITION BY [o0].[OrderID] ORDER BY [o0].[OrderID]) AS [row] + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE [o0].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task Include_collection_Join_GroupBy_Select(bool async) + { + await base.Include_collection_Join_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + INNER JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + INNER JOIN [Order Details] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]", + // + @"SELECT [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice], [t].[OrderID], [t0].[OrderID] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + INNER JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID] + FROM ( + SELECT [o1].[OrderID], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + INNER JOIN [Order Details] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +INNER JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]"); + } + + public override async Task Include_reference_Join_GroupBy_Select(bool async) + { + await base.Include_reference_Join_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + INNER JOIN [Order Details] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[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], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + INNER JOIN [Order Details] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + LEFT JOIN [Customers] AS [c] ON [o1].[CustomerID] = [c].[CustomerID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task Join_Include_collection_GroupBy_Select(bool async) + { + await base.Join_Include_collection_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + INNER JOIN [Orders] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]", + // + @"SELECT [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice], [t].[OrderID], [t0].[OrderID] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID] + FROM ( + SELECT [o2].[OrderID], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + INNER JOIN [Orders] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +INNER JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]"); + } + + public override async Task Join_Include_reference_GroupBy_Select(bool async) + { + await base.Join_Include_reference_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[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], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + INNER JOIN [Orders] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] + LEFT JOIN [Customers] AS [c] ON [o2].[CustomerID] = [c].[CustomerID] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task Include_collection_SelectMany_GroupBy_Select(bool async) + { + await base.Include_collection_SelectMany_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + CROSS JOIN [Order Details] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + CROSS JOIN [Order Details] AS [o2] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]", + // + @"SELECT [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice], [t].[OrderID], [t0].[OrderID] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + CROSS JOIN [Order Details] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID] + FROM ( + SELECT [o1].[OrderID], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + CROSS JOIN [Order Details] AS [o2] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +INNER JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]"); + } + + public override async Task Include_reference_SelectMany_GroupBy_Select(bool async) + { + await base.Include_reference_SelectMany_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o].[OrderID] + FROM [Orders] AS [o] + CROSS JOIN [Order Details] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o1].[OrderID], [o1].[CustomerID], [o1].[EmployeeID], [o1].[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], ROW_NUMBER() OVER(PARTITION BY [o1].[OrderID] ORDER BY [o1].[OrderID]) AS [row] + FROM [Orders] AS [o1] + CROSS JOIN [Order Details] AS [o2] + LEFT JOIN [Customers] AS [c] ON [o1].[CustomerID] = [c].[CustomerID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + + public override async Task SelectMany_Include_collection_GroupBy_Select(bool async) + { + await base.SelectMany_Include_collection_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t].[OrderID] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + CROSS JOIN [Orders] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[OrderDate], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + CROSS JOIN [Orders] AS [o2] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]", + // + @"SELECT [o3].[OrderID], [o3].[ProductID], [o3].[Discount], [o3].[Quantity], [o3].[UnitPrice], [t].[OrderID], [t0].[OrderID] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + CROSS JOIN [Orders] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID] + FROM ( + SELECT [o2].[OrderID], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + CROSS JOIN [Orders] AS [o2] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID] +INNER JOIN [Order Details] AS [o3] ON [t0].[OrderID] = [o3].[OrderID] +ORDER BY [t].[OrderID], [t0].[OrderID]"); + } + + public override async Task SelectMany_Include_reference_GroupBy_Select(bool async) + { + await base.SelectMany_Include_reference_GroupBy_Select(async); + + AssertSql( + @"SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[CustomerID0], [t0].[Address], [t0].[City], [t0].[CompanyName], [t0].[ContactName], [t0].[ContactTitle], [t0].[Country], [t0].[Fax], [t0].[Phone], [t0].[PostalCode], [t0].[Region] +FROM ( + SELECT [o0].[OrderID] + FROM [Order Details] AS [o] + CROSS JOIN [Orders] AS [o0] + WHERE [o].[OrderID] = 10248 + GROUP BY [o0].[OrderID] +) AS [t] +LEFT JOIN ( + SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0], [t1].[Address], [t1].[City], [t1].[CompanyName], [t1].[ContactName], [t1].[ContactTitle], [t1].[Country], [t1].[Fax], [t1].[Phone], [t1].[PostalCode], [t1].[Region] + FROM ( + SELECT [o2].[OrderID], [o2].[CustomerID], [o2].[EmployeeID], [o2].[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], ROW_NUMBER() OVER(PARTITION BY [o2].[OrderID] ORDER BY [o2].[OrderID]) AS [row] + FROM [Order Details] AS [o1] + CROSS JOIN [Orders] AS [o2] + LEFT JOIN [Customers] AS [c] ON [o2].[CustomerID] = [c].[CustomerID] + WHERE [o1].[OrderID] = 10248 + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[OrderID] = [t0].[OrderID]"); + } + public override async Task Include_reference_distinct_is_server_evaluated(bool async) { await base.Include_reference_distinct_is_server_evaluated(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index 8fbd40a241c..68286db5f89 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -1075,13 +1075,13 @@ public override async Task GroupBy_with_multiple_aggregates_on_owned_navigation_ await base.GroupBy_with_multiple_aggregates_on_owned_navigation_properties(async); AssertSql( - @"SELECT AVG(CAST([t].[Id] AS float)) AS [p1], COALESCE(SUM([t].[Id]), 0) AS [p2], MAX(CAST(LEN([t].[Name]) AS int)) AS [p3] + @"SELECT AVG(CAST([s].[Id] AS float)) AS [p1], COALESCE(SUM([s].[Id]), 0) AS [p2], MAX(CAST(LEN([s].[Name]) AS int)) AS [p3] FROM ( - SELECT [s].[Id], [s].[Name], 1 AS [Key] + SELECT 1 AS [Key], [o].[PersonAddress_Country_PlanetId] FROM [OwnedPerson] AS [o] - LEFT JOIN [Planet] AS [p] ON [o].[PersonAddress_Country_PlanetId] = [p].[Id] - LEFT JOIN [Star] AS [s] ON [p].[StarId] = [s].[Id] ) AS [t] +LEFT JOIN [Planet] AS [p] ON [t].[PersonAddress_Country_PlanetId] = [p].[Id] +LEFT JOIN [Star] AS [s] ON [p].[StarId] = [s].[Id] GROUP BY [t].[Key]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 34aff4a0ac5..32e5e606517 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -6509,11 +6509,25 @@ public override async Task Group_by_with_include_with_entity_in_result_selector( await base.Group_by_with_include_with_entity_in_result_selector(async); AssertSql( - @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [g.CityOfBirth].[Name], [g.CityOfBirth].[Location], [g.CityOfBirth].[Nation] -FROM [Gears] AS [g] -INNER JOIN [Cities] AS [g.CityOfBirth] ON [g].[CityOfBirthName] = [g.CityOfBirth].[Name] -WHERE [g].[Discriminator] IN (N'Officer', N'Gear') -ORDER BY [g].[Rank]"); + @"SELECT [t].[Rank], [t].[c], [t0].[Nickname], [t0].[SquadId], [t0].[AssignedCityName], [t0].[CityOfBirthName], [t0].[FullName], [t0].[HasSoulPatch], [t0].[LeaderNickname], [t0].[LeaderSquadId], [t0].[Rank], [t0].[Discriminator], [t0].[Name], [t0].[Location], [t0].[Nation] +FROM ( + SELECT [g].[Rank], COUNT(*) AS [c] + FROM [Gears] AS [g] + GROUP BY [g].[Rank] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Nickname], [t1].[SquadId], [t1].[AssignedCityName], [t1].[CityOfBirthName], [t1].[FullName], [t1].[HasSoulPatch], [t1].[LeaderNickname], [t1].[LeaderSquadId], [t1].[Rank], [t1].[Discriminator], [t1].[Name], [t1].[Location], [t1].[Nation] + FROM ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], CASE + WHEN [o0].[Nickname] IS NOT NULL THEN N'Officer' + END AS [Discriminator], [c].[Name], [c].[Location], [c].[Nation], ROW_NUMBER() OVER(PARTITION BY [g0].[Rank] ORDER BY [g0].[Nickname]) AS [row] + FROM [Gears] AS [g0] + LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) + INNER JOIN [Cities] AS [c] ON [g0].[CityOfBirthName] = [c].[Name] + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Rank] = [t0].[Rank] +ORDER BY [t].[Rank]"); } public override async Task GroupBy_Property_Include_Select_Max(bool async) @@ -6531,10 +6545,25 @@ public override async Task Include_with_group_by_and_FirstOrDefault_gets_properl await base.Include_with_group_by_and_FirstOrDefault_gets_properly_applied(async); AssertSql( - @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [g.CityOfBirth].[Name], [g.CityOfBirth].[Location], [g.CityOfBirth].[Nation] -FROM [Gears] AS [g] -INNER JOIN [Cities] AS [g.CityOfBirth] ON [g].[CityOfBirthName] = [g.CityOfBirth].[Name] -ORDER BY [g].[Rank]"); + @"SELECT [t0].[Nickname], [t0].[SquadId], [t0].[AssignedCityName], [t0].[CityOfBirthName], [t0].[FullName], [t0].[HasSoulPatch], [t0].[LeaderNickname], [t0].[LeaderSquadId], [t0].[Rank], [t0].[Discriminator], [t0].[Name], [t0].[Location], [t0].[Nation] +FROM ( + SELECT [g].[Rank] + FROM [Gears] AS [g] + GROUP BY [g].[Rank] +) AS [t] +LEFT JOIN ( + SELECT [t1].[Nickname], [t1].[SquadId], [t1].[AssignedCityName], [t1].[CityOfBirthName], [t1].[FullName], [t1].[HasSoulPatch], [t1].[LeaderNickname], [t1].[LeaderSquadId], [t1].[Rank], [t1].[Discriminator], [t1].[Name], [t1].[Location], [t1].[Nation] + FROM ( + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], CASE + WHEN [o0].[Nickname] IS NOT NULL THEN N'Officer' + END AS [Discriminator], [c].[Name], [c].[Location], [c].[Nation], ROW_NUMBER() OVER(PARTITION BY [g0].[Rank] ORDER BY [g0].[Rank], [c].[Name]) AS [row] + FROM [Gears] AS [g0] + LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) + INNER JOIN [Cities] AS [c] ON [g0].[CityOfBirthName] = [c].[Name] + WHERE [g0].[HasSoulPatch] = CAST(1 AS bit) + ) AS [t1] + WHERE [t1].[row] <= 1 +) AS [t0] ON [t].[Rank] = [t0].[Rank]"); } public override async Task Include_collection_with_Cast_to_base(bool async) @@ -7421,16 +7450,16 @@ public override async Task Complex_GroupBy_after_set_operator(bool async) FROM ( SELECT [c].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w0] - WHERE [g].[FullName] = [w0].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName]) AS [Count] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name] UNION ALL SELECT [c0].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w] - WHERE [g0].[FullName] = [w].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w0] + WHERE [g0].[FullName] = [w0].[OwnerFullName]) AS [Count] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) INNER JOIN [Cities] AS [c0] ON [g0].[CityOfBirthName] = [c0].[Name] @@ -7447,16 +7476,16 @@ public override async Task Complex_GroupBy_after_set_operator_using_result_selec FROM ( SELECT [c].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w0] - WHERE [g].[FullName] = [w0].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName]) AS [Count] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) LEFT JOIN [Cities] AS [c] ON [g].[AssignedCityName] = [c].[Name] UNION ALL SELECT [c0].[Name], ( SELECT COUNT(*) - FROM [Weapons] AS [w] - WHERE [g0].[FullName] = [w].[OwnerFullName]) AS [Count] + FROM [Weapons] AS [w0] + WHERE [g0].[FullName] = [w0].[OwnerFullName]) AS [Count] FROM [Gears] AS [g0] LEFT JOIN [Officers] AS [o0] ON ([g0].[Nickname] = [o0].[Nickname]) AND ([g0].[SquadId] = [o0].[SquadId]) INNER JOIN [Cities] AS [c0] ON [g0].[CityOfBirthName] = [c0].[Name] @@ -7515,8 +7544,6 @@ public override async Task Group_by_over_projection_with_multiple_properties_acc @"SELECT [c].[Name] FROM [Gears] AS [g] INNER JOIN [Cities] AS [c] ON [g].[CityOfBirthName] = [c].[Name] -LEFT JOIN [Cities] AS [c0] ON [g].[AssignedCityName] = [c0].[Name] -INNER JOIN [Squads] AS [s] ON [g].[SquadId] = [s].[Id] GROUP BY [c].[Name]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs index 27a575063c6..3df55f93984 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs @@ -125,15 +125,15 @@ public override async Task Can_query_shared_derived_hierarchy() SELECT [v1].[Name], [v1].[Capacity], [v1].[FuelTank_Discriminator], [v1].[FuelType], [v1].[GrainGeometry] FROM [Vehicles] AS [v1] INNER JOIN ( - SELECT [v2].[Name], [v2].[Computed], [v2].[Description], [v2].[Engine_Discriminator], [t1].[Name] AS [Name0] + SELECT [v2].[Name], [v2].[Computed], [v2].[Description], [v2].[Engine_Discriminator], [t2].[Name] AS [Name0] FROM [Vehicles] AS [v2] INNER JOIN ( SELECT [v3].[Name], [v3].[Discriminator], [v3].[SeatingCapacity], [v3].[AttachedVehicleName] FROM [Vehicles] AS [v3] WHERE [v3].[Discriminator] IN (N'PoweredVehicle', N'CompositeVehicle') - ) AS [t1] ON [v2].[Name] = [t1].[Name] + ) AS [t2] ON [v2].[Name] = [t2].[Name] WHERE [v2].[Engine_Discriminator] IN (N'ContinuousCombustionEngine', N'IntermittentCombustionEngine', N'SolidRocket') -) AS [t0] ON [v1].[Name] = [t0].[Name] +) AS [t1] ON [v1].[Name] = [t1].[Name] WHERE [v1].[Capacity] IS NOT NULL AND [v1].[FuelTank_Discriminator] IS NOT NULL"); } @@ -154,15 +154,15 @@ public override async Task Can_query_shared_derived_nonhierarchy() SELECT [v1].[Name], [v1].[Capacity], [v1].[FuelType] FROM [Vehicles] AS [v1] INNER JOIN ( - SELECT [v2].[Name], [v2].[Computed], [v2].[Description], [v2].[Engine_Discriminator], [t1].[Name] AS [Name0] + SELECT [v2].[Name], [v2].[Computed], [v2].[Description], [v2].[Engine_Discriminator], [t2].[Name] AS [Name0] FROM [Vehicles] AS [v2] INNER JOIN ( SELECT [v3].[Name], [v3].[Discriminator], [v3].[SeatingCapacity], [v3].[AttachedVehicleName] FROM [Vehicles] AS [v3] WHERE [v3].[Discriminator] IN (N'PoweredVehicle', N'CompositeVehicle') - ) AS [t1] ON [v2].[Name] = [t1].[Name] + ) AS [t2] ON [v2].[Name] = [t2].[Name] WHERE [v2].[Engine_Discriminator] IN (N'ContinuousCombustionEngine', N'IntermittentCombustionEngine', N'SolidRocket') -) AS [t0] ON [v1].[Name] = [t0].[Name] +) AS [t1] ON [v1].[Name] = [t1].[Name] WHERE [v1].[Capacity] IS NOT NULL"); } @@ -183,15 +183,15 @@ public override async Task Can_query_shared_derived_nonhierarchy_all_required() SELECT [v1].[Name], [v1].[Capacity], [v1].[FuelType] FROM [Vehicles] AS [v1] INNER JOIN ( - SELECT [v2].[Name], [v2].[Computed], [v2].[Description], [v2].[Engine_Discriminator], [t1].[Name] AS [Name0] + SELECT [v2].[Name], [v2].[Computed], [v2].[Description], [v2].[Engine_Discriminator], [t2].[Name] AS [Name0] FROM [Vehicles] AS [v2] INNER JOIN ( SELECT [v3].[Name], [v3].[Discriminator], [v3].[SeatingCapacity], [v3].[AttachedVehicleName] FROM [Vehicles] AS [v3] WHERE [v3].[Discriminator] IN (N'PoweredVehicle', N'CompositeVehicle') - ) AS [t1] ON [v2].[Name] = [t1].[Name] + ) AS [t2] ON [v2].[Name] = [t2].[Name] WHERE [v2].[Engine_Discriminator] IN (N'ContinuousCombustionEngine', N'IntermittentCombustionEngine', N'SolidRocket') -) AS [t0] ON [v1].[Name] = [t0].[Name] +) AS [t1] ON [v1].[Name] = [t1].[Name] WHERE [v1].[Capacity] IS NOT NULL AND [v1].[FuelType] IS NOT NULL"); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs index 075f7973507..a7fecc7253c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs @@ -37,5 +37,35 @@ public override async Task Select_uncorrelated_collection_with_groupby_when_oute SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync( () => base.Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(async))).Message); + + public override async Task AsEnumerable_in_subquery_for_GroupBy(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.AsEnumerable_in_subquery_for_GroupBy(async))).Message); + + public override async Task Complex_query_with_groupBy_in_subquery1(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Complex_query_with_groupBy_in_subquery1(async))).Message); + + public override async Task Complex_query_with_groupBy_in_subquery2(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Complex_query_with_groupBy_in_subquery2(async))).Message); + + public override async Task Complex_query_with_groupBy_in_subquery3(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Complex_query_with_groupBy_in_subquery3(async))).Message); + + public override async Task Select_nested_collection_with_groupby(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Select_nested_collection_with_groupby(async))).Message); } }