From 9b181c94d95b4f5b3f81bce6dc0031274d22784d Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Thu, 28 Apr 2022 14:51:57 -0700 Subject: [PATCH] Query: Translate aggregate over grouping element in separate pass Design: - Introduce `SqlEnumerableExpression` - a holder class which indicates the `SqlExpression` is in form of a enumerable (or group) coming as a result of whole table selection or a grouping element. It also stores details about if `Distinct` is applied over grouping or if there are any orderings. - Due to above `DistinctExpression` has been removed. The token while used to denote `Distinct` over grouping element were not valid in other parts of SQL tree hence it makes more sense to combine it with `SqlEnumerableExpression`. - To support dual pass, `GroupByShaperExpression` contains 2 forms of grouping element. One element selector form which correlates directly with the parent grouped query, second subquery form which correlates to parent grouped query through a correlation predicate. Element selector is first used to translate aggregation. If that fails we use subquery form to translate as a subquery. Due to 2 forms of same component, GroupByShaperExpression disallows calling into VisitChildren method, any visitor which is visiting a tree containing GroupByShaperExpression (which appears only in `QueryExpression.ShaperExpression` or LINQ expression after remapping but before translation) must intercept the tree and either ignore or process it appropriately. - An internal visitor (`GroupByAggregateChainProcessor`) inside SqlTranslator visits and process chain of queryable operations on a grouping element before aggregate is called and condense it into `SqlEnumerableExpression` which is then passed to method which translates aggregate. This visitor only processes Where/Distinct/Select for now. Future PR will add processing for OrderBy/ThenBy(Descending) operations to generate orderings. - Side-effect above is that joins expanded over the grouping element (due to navigations used on aggregate chain), doesn't translate to aggregate anymore since we need to translate the join on parent query, remove the translated join if the chain didn't end in aggregate and also de-dupe same joins. Filing issue to improve this in future. Due to fragile nature of matching to lift the join, we shouldn't try to lift joins. - To support custom aggregate operations, we will either reused `IMethodCallTranslator` or create a parallel structure for aggregate methods and call into it from SqlTranslator by passing translated SqlEnumerableExpression as appropriate. - For complex grouping key, we cause a pushdown so that we can reference the grouping key through columns only. This allows us to reference the grouping key in correlation predicate for subquery without generating invalid SQL in many cases. - With complex grouping key converting to columns, now we are able to correctly generate identifiers for grouping queries which makes more queries with correlated collections (where either parent or inner both queries can be groupby query) translatable. - Erase client projection when applying aggregate operation over GroupBy result. - When processing result selector in GroupBy use the updated key selector if the select expression was pushed down. Resolves #27132 Resolves #27266 Resolves #27433 Resolves #23601 Resolves #27721 Resolves #27796 Resolves #27801 Resolves #19683 Relates to #22957 --- .../Query/Internal/InMemoryQueryExpression.cs | 1 + ...yableMethodTranslatingExpressionVisitor.cs | 1 + ...ionalProjectionBindingExpressionVisitor.cs | 83 ++- .../Query/QuerySqlGenerator.cs | 35 +- ...yableMethodTranslatingExpressionVisitor.cs | 113 +--- ...lationalSqlTranslatingExpressionVisitor.cs | 500 +++++++++++++----- .../Query/SqlExpressionFactory.cs | 25 +- .../Query/SqlExpressionVisitor.cs | 30 +- .../SqlExpressions/DistinctExpression.cs | 69 --- .../SqlExpressions/SelectExpression.Helper.cs | 280 +++------- .../Query/SqlExpressions/SelectExpression.cs | 49 +- .../SqlExpressions/SqlEnumerableExpression.cs | 119 +++++ .../Query/SqlExpressions/SqlExpression.cs | 3 + .../Query/SqlNullabilityProcessor.cs | 45 +- ...rchConditionConvertingExpressionVisitor.cs | 49 +- ...qlServerSqlTranslatingExpressionVisitor.cs | 4 +- .../SqliteSqlTranslatingExpressionVisitor.cs | 16 +- src/EFCore/Query/GroupByShaperExpression.cs | 39 +- .../Query/ReplacingExpressionVisitor.cs | 3 +- .../Query/GearsOfWarQueryInMemoryTest.cs | 8 - .../GearsOfWarQueryRelationalTestBase.cs | 10 - ...NorthwindGroupByQueryRelationalTestBase.cs | 18 - .../NorthwindSelectQueryRelationalTestBase.cs | 9 - .../Query/GearsOfWarQueryTestBase.cs | 2 +- .../Query/NorthwindGroupByQueryTestBase.cs | 193 ++++++- .../ComplexNavigationsQuerySqlServerTest.cs | 33 +- ...NavigationsSharedTypeQuerySqlServerTest.cs | 217 +++++--- .../Query/Ef6GroupBySqlServerTest.cs | 16 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 76 +-- .../NorthwindGroupByQuerySqlServerTest.cs | 235 ++++++-- .../NorthwindSelectQuerySqlServerTest.cs | 38 +- .../Query/OwnedQuerySqlServerTest.cs | 30 +- .../Query/SimpleQuerySqlServerTest.cs | 106 ++-- .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 76 +-- .../TemporalGearsOfWarQuerySqlServerTest.cs | 58 +- .../Query/TemporalOwnedQuerySqlServerTest.cs | 30 +- .../Query/GearsOfWarQuerySqliteTest.cs | 33 +- .../Query/NorthwindGroupByQuerySqliteTest.cs | 6 + .../Query/NorthwindSelectQuerySqliteTest.cs | 6 + .../Query/TPTGearsOfWarQuerySqliteTest.cs | 6 + 40 files changed, 1659 insertions(+), 1011 deletions(-) delete mode 100644 src/EFCore.Relational/Query/SqlExpressions/DistinctExpression.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/SqlEnumerableExpression.cs diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs index 31b864eb51f..7abfecc053d 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs @@ -600,6 +600,7 @@ public virtual GroupByShaperExpression ApplyGrouping( return new GroupByShaperExpression( groupingKey, + shaperExpression, new ShapedQueryExpression( clonedInMemoryQueryExpression, new QueryExpressionReplacingExpressionVisitor(this, clonedInMemoryQueryExpression).Visit(shaperExpression))); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index ebfe4334114..611ce48d589 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -1192,6 +1192,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp protected override Expression VisitExtension(Expression extensionExpression) => extensionExpression is EntityShaperExpression || extensionExpression is ShapedQueryExpression + || extensionExpression is GroupByShaperExpression ? extensionExpression : base.VisitExtension(extensionExpression); diff --git a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs index 7be353baa27..5022ca16d4b 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs @@ -146,63 +146,58 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio new ProjectionBindingExpression(_selectExpression, _clientProjections.Count - 1, expression.Type), materializeCollectionNavigationExpression.Navigation, materializeCollectionNavigationExpression.Navigation.ClrType.GetSequenceType()); + } + + var translation = _sqlTranslator.Translate(expression); + if (translation != null) + { + return AddClientProjection(translation, expression.Type.MakeNullable()); + } - case MethodCallExpression methodCallExpression: - if (methodCallExpression.Method.IsGenericMethod + if (expression is MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Method.IsGenericMethod && methodCallExpression.Method.DeclaringType == typeof(Enumerable) && methodCallExpression.Method.Name == nameof(Enumerable.ToList) && methodCallExpression.Arguments.Count == 1 && methodCallExpression.Arguments[0].Type.TryGetElementType(typeof(IQueryable<>)) != null) + { + var subquery = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery( + methodCallExpression.Arguments[0]); + if (subquery != null) { - var subquery = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery( - methodCallExpression.Arguments[0]); - if (subquery != null) - { - _clientProjections!.Add(subquery); - // expression.Type here will be List - return new CollectionResultExpression( - new ProjectionBindingExpression(_selectExpression, _clientProjections.Count - 1, expression.Type), - navigation: null, - methodCallExpression.Method.GetGenericArguments()[0]); - } + _clientProjections!.Add(subquery); + // expression.Type here will be List + return new CollectionResultExpression( + new ProjectionBindingExpression(_selectExpression, _clientProjections.Count - 1, expression.Type), + navigation: null, + methodCallExpression.Method.GetGenericArguments()[0]); } - else + } + else + { + var subquery = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(methodCallExpression); + if (subquery != null) { - var subquery = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(methodCallExpression); - if (subquery != null) + _clientProjections!.Add(subquery); + var type = expression.Type; + if (type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(IQueryable<>)) { - // This simplifies the check when subquery is translated and can be lifted as scalar. - var scalarTranslation = _sqlTranslator.Translate(subquery); - if (scalarTranslation != null) - { - return AddClientProjection(scalarTranslation, expression.Type.MakeNullable()); - } - - _clientProjections!.Add(subquery); - var type = expression.Type; - - if (type.IsGenericType - && type.GetGenericTypeDefinition() == typeof(IQueryable<>)) - { - type = typeof(List<>).MakeGenericType(type.GetSequenceType()); - } - - var projectionBindingExpression = new ProjectionBindingExpression( - _selectExpression, _clientProjections.Count - 1, type); - return subquery.ResultCardinality == ResultCardinality.Enumerable - ? new CollectionResultExpression( - projectionBindingExpression, navigation: null, subquery.ShaperExpression.Type) - : projectionBindingExpression; + type = typeof(List<>).MakeGenericType(type.GetSequenceType()); } - } - break; + var projectionBindingExpression = new ProjectionBindingExpression( + _selectExpression, _clientProjections.Count - 1, type); + return subquery.ResultCardinality == ResultCardinality.Enumerable + ? new CollectionResultExpression( + projectionBindingExpression, navigation: null, subquery.ShaperExpression.Type) + : projectionBindingExpression; + } + } } - var translation = _sqlTranslator.Translate(expression); - return translation != null - ? AddClientProjection(translation, expression.Type.MakeNullable()) - : base.Visit(expression); + return base.Visit(expression); } else { diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 1e651ea198d..abe0dda552d 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -506,6 +506,31 @@ protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpres return sqlBinaryExpression; } + /// + protected override Expression VisitSqlEnumerable(SqlEnumerableExpression sqlEnumerableExpression) + { + if (sqlEnumerableExpression.Orderings.Count != 0) + { + // TODO: Throw error here because we don't know how to print orderings. + // Though providers can override this method and generate orderings if they have a way to print it. + throw new InvalidOperationException(); + } + + if (sqlEnumerableExpression.IsDistinct) + { + _relationalCommandBuilder.Append("DISTINCT ("); + } + + Visit(sqlEnumerableExpression.SqlExpression); + + if (sqlEnumerableExpression.IsDistinct) + { + _relationalCommandBuilder.Append(")"); + } + + return sqlEnumerableExpression; + } + /// protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) { @@ -609,16 +634,6 @@ protected override Expression VisitCollate(CollateExpression collateExpression) return collateExpression; } - /// - protected override Expression VisitDistinct(DistinctExpression distinctExpression) - { - _relationalCommandBuilder.Append("DISTINCT ("); - Visit(distinctExpression.Operand); - _relationalCommandBuilder.Append(")"); - - return distinctExpression; - } - /// protected override Expression VisitCase(CaseExpression caseExpression) { diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index b8bad0935f7..2e1ff734740 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -17,7 +17,6 @@ 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. @@ -135,7 +134,6 @@ when queryRootExpression.GetType() == typeof(QueryRootExpression) case GroupByShaperExpression groupByShaperExpression: var groupShapedQueryExpression = groupByShaperExpression.GroupingEnumerable; var groupClonedSelectExpression = ((SelectExpression)groupShapedQueryExpression.QueryExpression).Clone(); - _groupingElementCorrelationalPredicate = groupClonedSelectExpression.Predicate; return new ShapedQueryExpression( groupClonedSelectExpression, new QueryExpressionReplacingExpressionVisitor( @@ -418,7 +416,7 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent var newResultSelectorBody = new ReplacingExpressionVisitor( new Expression[] { original1, original2 }, - new[] { translatedKey, groupByShaper }) + new[] { groupByShaper.KeySelector, groupByShaper }) .Visit(resultSelector.Body); newResultSelectorBody = ExpandSharedTypeEntities(selectExpression, newResultSelectorBody); @@ -1032,6 +1030,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp protected override Expression VisitExtension(Expression extensionExpression) => extensionExpression is EntityShaperExpression || extensionExpression is ShapedQueryExpression + || extensionExpression is GroupByShaperExpression ? extensionExpression : base.VisitExtension(extensionExpression); @@ -1356,7 +1355,7 @@ public DeferredOwnedExpansionRemovingVisitor(SelectExpression selectExpression) { DeferredOwnedExpansionExpression doee => UnwrapDeferredEntityProjectionExpression(doee), // For the source entity shaper or owned collection expansion - EntityShaperExpression or ShapedQueryExpression => expression, + EntityShaperExpression or ShapedQueryExpression or GroupByShaperExpression => expression, _ => base.Visit(expression) }; @@ -1406,7 +1405,8 @@ private static void HandleGroupByForAggregate(SelectExpression selectExpression, { if (eraseProjection) { - selectExpression.ReplaceProjection(new Dictionary()); + // Erasing client projections erase projectionMapping projections too + selectExpression.ReplaceProjection(new List()); } selectExpression.PushdownIntoSubquery(); @@ -1461,14 +1461,11 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape private ShapedQueryExpression? TranslateAggregateWithPredicate( ShapedQueryExpression source, LambdaExpression? predicate, - Func aggregateTranslator, + Func aggregateTranslator, Type resultType) { var selectExpression = (SelectExpression)source.QueryExpression; - if (_groupingElementCorrelationalPredicate == null) - { - selectExpression.PrepareForAggregate(); - } + selectExpression.PrepareForAggregate(); if (predicate != null) { @@ -1481,37 +1478,9 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape 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); - } + HandleGroupByForAggregate(selectExpression, eraseProjection: true); - var translation = aggregateTranslator(sqlExpression); + var translation = aggregateTranslator(new SqlEnumerableExpression(_sqlExpressionFactory.Fragment("*"), distinct: false, null)); if (translation == null) { return null; @@ -1531,16 +1500,13 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape private ShapedQueryExpression? TranslateAggregateWithSelector( ShapedQueryExpression source, LambdaExpression? selector, - Func aggregateTranslator, + Func aggregateTranslator, bool throwWhenEmpty, Type resultType) { var selectExpression = (SelectExpression)source.QueryExpression; - if (_groupingElementCorrelationalPredicate == null) - { - selectExpression.PrepareForAggregate(); - HandleGroupByForAggregate(selectExpression); - } + selectExpression.PrepareForAggregate(); + HandleGroupByForAggregate(selectExpression); SqlExpression translatedSelector; if (selector == null @@ -1575,12 +1541,7 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape } } - if (_groupingElementCorrelationalPredicate != null) - { - translatedSelector = CombineGroupByAggregateTerms(selectExpression, translatedSelector); - } - - var projection = aggregateTranslator(translatedSelector); + var projection = aggregateTranslator(new SqlEnumerableExpression(translatedSelector, distinct: false, null)); if (projection == null) { return null; @@ -1636,52 +1597,4 @@ private static Expression MatchShaperNullabilityForSetOperation(Expression shape return source.UpdateShaperExpression(shaper); } - - private SqlExpression CombineGroupByAggregateTerms(SelectExpression selectExpression, SqlExpression selector) - { - if (selectExpression.Predicate != null - && !selectExpression.Predicate.Equals(_groupingElementCorrelationalPredicate)) - { - if (selector is 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(predicate, selector) }, - elseResult: null); - selectExpression.UpdatePredicate(_groupingElementCorrelationalPredicate!); - } - - if (selectExpression.IsDistinct) - { - if (selector is SqlFragmentExpression { Sql: "*" }) - { - selector = _sqlExpressionFactory.Constant(1); - } - - selector = new DistinctExpression(selector); - } - - return selector; - - static void PopulatePredicateTerms(SqlExpression predicate, List terms) - { - if (predicate is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } sqlBinaryExpression) - { - PopulatePredicateTerms(sqlBinaryExpression.Left, terms); - PopulatePredicateTerms(sqlBinaryExpression.Right, terms); - } - else - { - terms.Add(predicate); - } - } - } } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 1281515b1b1..a3855ff22b6 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -39,6 +39,14 @@ public class RelationalSqlTranslatingExpressionVisitor : ExpressionVisitor //QueryableMethodProvider.ElementAtOrDefaultMethodInfo }; + private static readonly List PredicateAggregateMethodInfos = new() + { + QueryableMethods.CountWithPredicate, + QueryableMethods.CountWithoutPredicate, + QueryableMethods.LongCountWithPredicate, + QueryableMethods.LongCountWithoutPredicate + }; + private static readonly MethodInfo ParameterValueExtractorMethod = typeof(RelationalSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!; @@ -59,6 +67,7 @@ private static readonly MethodInfo ObjectEqualsMethodInfo private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly QueryableMethodTranslatingExpressionVisitor _queryableMethodTranslatingExpressionVisitor; private readonly SqlTypeMappingVerifyingExpressionVisitor _sqlTypeMappingVerifyingExpressionVisitor; + private readonly GroupByAggregateChainProcessor _groupByAggregateChainProcessor; /// /// Creates a new instance of the class. @@ -77,6 +86,7 @@ public RelationalSqlTranslatingExpressionVisitor( _model = queryCompilationContext.Model; _queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor; _sqlTypeMappingVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor(); + _groupByAggregateChainProcessor = new GroupByAggregateChainProcessor(this); } /// @@ -149,51 +159,50 @@ protected virtual void AddTranslationErrorDetails(string details) /// /// Translates Average over an expression to an equivalent SQL representation. /// - /// An expression to translate Average over. + /// An expression to translate Average over. /// A SQL translation of Average over the given expression. - public virtual SqlExpression? TranslateAverage(SqlExpression sqlExpression) + public virtual SqlExpression? TranslateAverage(SqlEnumerableExpression sqlEnumerableExpression) { - var inputType = sqlExpression.Type; + sqlEnumerableExpression = sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()); + var inputType = sqlEnumerableExpression.Type; if (inputType == typeof(int) || inputType == typeof(long)) { - sqlExpression = sqlExpression is DistinctExpression distinctExpression - ? new DistinctExpression( - _sqlExpressionFactory.ApplyDefaultTypeMapping( - _sqlExpressionFactory.Convert(distinctExpression.Operand, typeof(double)))) - : _sqlExpressionFactory.ApplyDefaultTypeMapping( - _sqlExpressionFactory.Convert(sqlExpression, typeof(double))); + sqlEnumerableExpression = sqlEnumerableExpression.Update( + _sqlExpressionFactory.ApplyDefaultTypeMapping( + _sqlExpressionFactory.Convert(sqlEnumerableExpression.SqlExpression, typeof(double))), + sqlEnumerableExpression.Orderings); } return inputType == typeof(float) ? _sqlExpressionFactory.Convert( _sqlExpressionFactory.Function( "AVG", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression }, nullable: true, argumentsPropagateNullability: new[] { false }, typeof(double)), - sqlExpression.Type, - sqlExpression.TypeMapping) + sqlEnumerableExpression.Type, + sqlEnumerableExpression.TypeMapping) : _sqlExpressionFactory.Function( "AVG", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression }, nullable: true, argumentsPropagateNullability: new[] { false }, - sqlExpression.Type, - sqlExpression.TypeMapping); + sqlEnumerableExpression.Type, + sqlEnumerableExpression.TypeMapping); } /// /// Translates Count over an expression to an equivalent SQL representation. /// - /// An expression to translate Count over. + /// An expression to translate Count over. /// A SQL translation of Count over the given expression. - public virtual SqlExpression? TranslateCount(SqlExpression sqlExpression) + public virtual SqlExpression? TranslateCount(SqlEnumerableExpression sqlEnumerableExpression) => _sqlExpressionFactory.ApplyDefaultTypeMapping( _sqlExpressionFactory.Function( "COUNT", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()) }, nullable: false, argumentsPropagateNullability: new[] { false }, typeof(int))); @@ -201,13 +210,13 @@ protected virtual void AddTranslationErrorDetails(string details) /// /// Translates LongCount over an expression to an equivalent SQL representation. /// - /// An expression to translate LongCount over. + /// An expression to translate LongCount over. /// A SQL translation of LongCount over the given expression. - public virtual SqlExpression? TranslateLongCount(SqlExpression sqlExpression) + public virtual SqlExpression? TranslateLongCount(SqlEnumerableExpression sqlEnumerableExpression) => _sqlExpressionFactory.ApplyDefaultTypeMapping( _sqlExpressionFactory.Function( "COUNT", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()) }, nullable: false, argumentsPropagateNullability: new[] { false }, typeof(long))); @@ -215,61 +224,61 @@ protected virtual void AddTranslationErrorDetails(string details) /// /// Translates Max over an expression to an equivalent SQL representation. /// - /// An expression to translate Max over. + /// An expression to translate Max over. /// A SQL translation of Max over the given expression. - public virtual SqlExpression? TranslateMax(SqlExpression sqlExpression) - => sqlExpression != null + public virtual SqlExpression? TranslateMax(SqlEnumerableExpression sqlEnumerableExpression) + => sqlEnumerableExpression != null ? _sqlExpressionFactory.Function( "MAX", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()) }, nullable: true, argumentsPropagateNullability: new[] { false }, - sqlExpression.Type, - sqlExpression.TypeMapping) + sqlEnumerableExpression.Type, + sqlEnumerableExpression.TypeMapping) : null; /// /// Translates Min over an expression to an equivalent SQL representation. /// - /// An expression to translate Min over. + /// An expression to translate Min over. /// A SQL translation of Min over the given expression. - public virtual SqlExpression? TranslateMin(SqlExpression sqlExpression) - => sqlExpression != null + public virtual SqlExpression? TranslateMin(SqlEnumerableExpression sqlEnumerableExpression) + => sqlEnumerableExpression != null ? _sqlExpressionFactory.Function( "MIN", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()) }, nullable: true, argumentsPropagateNullability: new[] { false }, - sqlExpression.Type, - sqlExpression.TypeMapping) + sqlEnumerableExpression.Type, + sqlEnumerableExpression.TypeMapping) : null; /// /// Translates Sum over an expression to an equivalent SQL representation. /// - /// An expression to translate Sum over. + /// An expression to translate Sum over. /// A SQL translation of Sum over the given expression. - public virtual SqlExpression? TranslateSum(SqlExpression sqlExpression) + public virtual SqlExpression? TranslateSum(SqlEnumerableExpression sqlEnumerableExpression) { - var inputType = sqlExpression.Type; + var inputType = sqlEnumerableExpression.Type; return inputType == typeof(float) ? _sqlExpressionFactory.Convert( _sqlExpressionFactory.Function( "SUM", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()) }, nullable: true, argumentsPropagateNullability: new[] { false }, typeof(double)), inputType, - sqlExpression.TypeMapping) + sqlEnumerableExpression.TypeMapping) : _sqlExpressionFactory.Function( "SUM", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()) }, nullable: true, argumentsPropagateNullability: new[] { false }, inputType, - sqlExpression.TypeMapping); + sqlEnumerableExpression.TypeMapping); } /// @@ -432,80 +441,6 @@ protected override Expression VisitExtension(Expression extensionExpression) return ((SelectExpression)projectionBindingExpression.QueryExpression) .GetProjection(projectionBindingExpression); - case ShapedQueryExpression shapedQueryExpression: - if (shapedQueryExpression.ResultCardinality == ResultCardinality.Enumerable) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - var shaperExpression = shapedQueryExpression.ShaperExpression; - ProjectionBindingExpression? mappedProjectionBindingExpression = null; - - var innerExpression = shaperExpression; - Type? convertedType = null; - if (shaperExpression is UnaryExpression unaryExpression - && unaryExpression.NodeType == ExpressionType.Convert) - { - convertedType = unaryExpression.Type; - innerExpression = unaryExpression.Operand; - } - - if (innerExpression is EntityShaperExpression ese - && (convertedType == null - || convertedType.IsAssignableFrom(ese.Type))) - { - return new EntityReferenceExpression(shapedQueryExpression.UpdateShaperExpression(innerExpression)); - } - - if (innerExpression is ProjectionBindingExpression pbe - && (convertedType == null - || convertedType.MakeNullable() == innerExpression.Type)) - { - mappedProjectionBindingExpression = pbe; - } - - if (mappedProjectionBindingExpression == null - && shaperExpression is BlockExpression blockExpression - && blockExpression.Expressions.Count == 2 - && blockExpression.Expressions[0] is BinaryExpression binaryExpression - && binaryExpression.NodeType == ExpressionType.Assign - && binaryExpression.Right is ProjectionBindingExpression pbe2) - { - mappedProjectionBindingExpression = pbe2; - } - - if (mappedProjectionBindingExpression == null) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - var subquery = (SelectExpression)shapedQueryExpression.QueryExpression; - var projection = subquery.GetProjection(mappedProjectionBindingExpression); - if (projection is not SqlExpression sqlExpression) - { - return QueryCompilationContext.NotTranslatedExpression; - } - - if (subquery.Tables.Count == 0) - { - return sqlExpression; - } - - subquery.ReplaceProjection(new List { sqlExpression }); - subquery.ApplyProjection(); - - SqlExpression scalarSubqueryExpression = new ScalarSubqueryExpression(subquery); - - if (shapedQueryExpression.ResultCardinality == ResultCardinality.SingleOrDefault - && !shaperExpression.Type.IsNullableType()) - { - scalarSubqueryExpression = _sqlExpressionFactory.Coalesce( - scalarSubqueryExpression, - (SqlExpression)Visit(shaperExpression.Type.GetDefaultValueConstant())); - } - - return scalarSubqueryExpression; - default: return QueryCompilationContext.NotTranslatedExpression; } @@ -561,10 +496,89 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } // Subquery case + var groupByAggregateTranslation = _groupByAggregateChainProcessor.Visit(methodCallExpression); + // TODO: In future refactor this so if arguments translate to SqlEnumerable but visitation fails, + // then we don't go on deeper level to translate it. + if (groupByAggregateTranslation != QueryCompilationContext.NotTranslatedExpression) + { + return groupByAggregateTranslation; + } + var subqueryTranslation = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(methodCallExpression); if (subqueryTranslation != null) { - return Visit(subqueryTranslation); + if (subqueryTranslation.ResultCardinality == ResultCardinality.Enumerable) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + var shaperExpression = subqueryTranslation.ShaperExpression; + ProjectionBindingExpression? mappedProjectionBindingExpression = null; + + var innerExpression = shaperExpression; + Type? convertedType = null; + if (shaperExpression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert) + { + convertedType = unaryExpression.Type; + innerExpression = unaryExpression.Operand; + } + + if (innerExpression is EntityShaperExpression ese + && (convertedType == null + || convertedType.IsAssignableFrom(ese.Type))) + { + return new EntityReferenceExpression(subqueryTranslation.UpdateShaperExpression(innerExpression)); + } + + if (innerExpression is ProjectionBindingExpression pbe + && (convertedType == null + || convertedType.MakeNullable() == innerExpression.Type)) + { + mappedProjectionBindingExpression = pbe; + } + + if (mappedProjectionBindingExpression == null + && shaperExpression is BlockExpression blockExpression + && blockExpression.Expressions.Count == 2 + && blockExpression.Expressions[0] is BinaryExpression binaryExpression + && binaryExpression.NodeType == ExpressionType.Assign + && binaryExpression.Right is ProjectionBindingExpression pbe2) + { + mappedProjectionBindingExpression = pbe2; + } + + if (mappedProjectionBindingExpression == null) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + var subquery = (SelectExpression)subqueryTranslation.QueryExpression; + var projection = subquery.GetProjection(mappedProjectionBindingExpression); + if (projection is not SqlExpression sqlExpression) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + if (subquery.Tables.Count == 0) + { + return sqlExpression; + } + + subquery.ReplaceProjection(new List { sqlExpression }); + subquery.ApplyProjection(); + + SqlExpression scalarSubqueryExpression = new ScalarSubqueryExpression(subquery); + + if (subqueryTranslation.ResultCardinality == ResultCardinality.SingleOrDefault + && !shaperExpression.Type.IsNullableType()) + { + scalarSubqueryExpression = _sqlExpressionFactory.Coalesce( + scalarSubqueryExpression, + (SqlExpression)Visit(shaperExpression.Type.GetDefaultValueConstant())); + } + + return scalarSubqueryExpression; } SqlExpression? sqlObject = null; @@ -933,10 +947,6 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) if (entityReferenceExpression.ParameterEntity != null) { var valueBufferExpression = Visit(entityReferenceExpression.ParameterEntity.ValueBufferExpression); - if (valueBufferExpression == QueryCompilationContext.NotTranslatedExpression) - { - return null; - } var entityProjectionExpression = (EntityProjectionExpression)valueBufferExpression; var propertyAccess = entityProjectionExpression.BindProperty(property); @@ -1457,12 +1467,262 @@ public Expression Convert(Type type) } } + private sealed class GroupByAggregateChainProcessor : ExpressionVisitor + { + private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslatingExpressionVisitor; + + public GroupByAggregateChainProcessor(RelationalSqlTranslatingExpressionVisitor sqlTranslatingExpressionVisitor) + { + _sqlTranslatingExpressionVisitor = sqlTranslatingExpressionVisitor; + } + + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Method.IsStatic + && methodCallExpression.Arguments.Count > 0 + && methodCallExpression.Method.DeclaringType == typeof(Queryable)) + { + if (methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable + && methodCallExpression.Arguments[0] is GroupByShaperExpression groupByShaperExpression) + { + return new GroupAggregatingElementExpression(groupByShaperExpression.ElementSelector); + } + + if (methodCallExpression.Arguments[0] is ShapedQueryExpression) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + var source = Visit(methodCallExpression.Arguments[0]); + if (source is GroupAggregatingElementExpression groupAggregatingElementExpression) + { + Expression? result = null; + switch (methodCallExpression.Method.Name) + { + case nameof(Queryable.Average): + if (methodCallExpression.Arguments.Count == 2) + { + ProcessSelector(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + } + + result = TranslateAggregate(methodCallExpression.Method, groupAggregatingElementExpression); + break; + + case nameof(Queryable.Count): + if (methodCallExpression.Arguments.Count == 2 + && !ProcessPredicate(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote())) + { + break; + } + + result = TranslateAggregate(methodCallExpression.Method, groupAggregatingElementExpression); + break; + + + case nameof(Queryable.Distinct): + result = groupAggregatingElementExpression.Element is EntityShaperExpression + ? groupAggregatingElementExpression + : groupAggregatingElementExpression.IsDistinct + ? null + : groupAggregatingElementExpression.ApplyDistinct(); + break; + + case nameof(Queryable.LongCount): + if (methodCallExpression.Arguments.Count == 2 + && !ProcessPredicate(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote())) + { + break; + } + + result = TranslateAggregate(methodCallExpression.Method, groupAggregatingElementExpression); + break; + + case nameof(Queryable.Max): + if (methodCallExpression.Arguments.Count == 2) + { + ProcessSelector(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + } + + result = TranslateAggregate(methodCallExpression.Method, groupAggregatingElementExpression); + break; + + case nameof(Queryable.Min): + if (methodCallExpression.Arguments.Count == 2) + { + ProcessSelector(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + } + + result = TranslateAggregate(methodCallExpression.Method, groupAggregatingElementExpression); + break; + + case nameof(Queryable.Select): + ProcessSelector(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + result = groupAggregatingElementExpression; + break; + + case nameof(Queryable.Sum): + if (methodCallExpression.Arguments.Count == 2) + { + ProcessSelector(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote()); + } + + result = TranslateAggregate(methodCallExpression.Method, groupAggregatingElementExpression); + break; + + case nameof(Queryable.Where): + if (ProcessPredicate(groupAggregatingElementExpression, methodCallExpression.Arguments[1].UnwrapLambdaFromQuote())) + { + result = groupAggregatingElementExpression; + } + break; + } + + if (result != null) + { + return result; + } + } + } + + return QueryCompilationContext.NotTranslatedExpression; + } + + private static void ProcessSelector( + GroupAggregatingElementExpression groupAggregatingElementExpression, LambdaExpression lambdaExpression) + { + var selector = RemapLambda(groupAggregatingElementExpression, lambdaExpression); + + groupAggregatingElementExpression.ApplySelector(selector); + } + + private static Expression RemapLambda( + GroupAggregatingElementExpression groupAggregatingElementExpression, LambdaExpression lambdaExpression) + => ReplacingExpressionVisitor.Replace( + lambdaExpression.Parameters[0], groupAggregatingElementExpression.Element, lambdaExpression.Body); + + private bool ProcessPredicate(GroupAggregatingElementExpression groupAggregatingElementExpression, LambdaExpression lambdaExpression) + { + var lambdaBody = RemapLambda(groupAggregatingElementExpression, lambdaExpression); + + var predicate = _sqlTranslatingExpressionVisitor.TranslateInternal(lambdaBody); + if (predicate == null) + { + return false; + } + + groupAggregatingElementExpression.ApplyPredicate(predicate); + + return true; + } + + private SqlExpression? TranslateAggregate(MethodInfo methodInfo, GroupAggregatingElementExpression groupAggregatingElementExpression) + { + var selector = _sqlTranslatingExpressionVisitor.TranslateInternal(groupAggregatingElementExpression.Element); + if (selector == null) + { + if (methodInfo.IsGenericMethod + && PredicateAggregateMethodInfos.Contains(methodInfo.GetGenericMethodDefinition())) + { + selector = _sqlTranslatingExpressionVisitor._sqlExpressionFactory.Fragment("*"); + } + else + { + return null; + } + } + + if (groupAggregatingElementExpression.Predicate != null) + { + if (selector is SqlFragmentExpression) + { + selector = _sqlTranslatingExpressionVisitor._sqlExpressionFactory.Constant(1); + } + + selector = _sqlTranslatingExpressionVisitor._sqlExpressionFactory.Case( + new List { new(groupAggregatingElementExpression.Predicate, selector) }, + elseResult: null); + } + + var sqlExpression = new SqlEnumerableExpression(selector, groupAggregatingElementExpression.IsDistinct, null); + + // TODO: Issue#22957 + return methodInfo.Name switch + { + nameof(Queryable.Average) => _sqlTranslatingExpressionVisitor.TranslateAverage(sqlExpression), + nameof(Queryable.Count) => _sqlTranslatingExpressionVisitor.TranslateCount(sqlExpression), + nameof(Queryable.LongCount) => _sqlTranslatingExpressionVisitor.TranslateLongCount(sqlExpression), + nameof(Queryable.Max) => _sqlTranslatingExpressionVisitor.TranslateMax(sqlExpression), + nameof(Queryable.Min) => _sqlTranslatingExpressionVisitor.TranslateMin(sqlExpression), + nameof(Queryable.Sum) => _sqlTranslatingExpressionVisitor.TranslateSum(sqlExpression), + _ => null, + }; + } + } + + private sealed class GroupAggregatingElementExpression : Expression + { + public GroupAggregatingElementExpression(Expression element) + { + Element = element; + } + + public Expression Element { get; private set; } + public bool IsDistinct { get; private set; } + public SqlExpression? Predicate { get; private set; } + + public GroupAggregatingElementExpression ApplyDistinct() + { + IsDistinct = true; + + return this; + } + + public GroupAggregatingElementExpression ApplySelector(Expression expression) + { + Element = expression; + + return this; + } + + public GroupAggregatingElementExpression 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) { if (extensionExpression is SqlExpression sqlExpression - && extensionExpression is not SqlFragmentExpression) + && extensionExpression is not SqlFragmentExpression + && !(extensionExpression is SqlEnumerableExpression sqlEnumerableExpression + && sqlEnumerableExpression.SqlExpression is SqlFragmentExpression)) { if (sqlExpression.TypeMapping == null) { diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index b2ccda6768b..6ba8b8c5e5a 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -56,15 +56,15 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) { CaseExpression e => ApplyTypeMappingOnCase(e, typeMapping), CollateExpression e => ApplyTypeMappingOnCollate(e, typeMapping), - DistinctExpression e => ApplyTypeMappingOnDistinct(e, typeMapping), + InExpression e => ApplyTypeMappingOnIn(e), LikeExpression e => ApplyTypeMappingOnLike(e), SqlBinaryExpression e => ApplyTypeMappingOnSqlBinary(e, typeMapping), - SqlUnaryExpression e => ApplyTypeMappingOnSqlUnary(e, typeMapping), SqlConstantExpression e => e.ApplyTypeMapping(typeMapping), + SqlEnumerableExpression e => ApplyTypeMappingOnSqlEnumerable(e, typeMapping), SqlFragmentExpression e => e, SqlFunctionExpression e => e.ApplyTypeMapping(typeMapping), SqlParameterExpression e => e.ApplyTypeMapping(typeMapping), - InExpression e => ApplyTypeMappingOnIn(e), + SqlUnaryExpression e => ApplyTypeMappingOnSqlUnary(e, typeMapping), _ => sqlExpression }; } @@ -108,11 +108,6 @@ private SqlExpression ApplyTypeMappingOnCollate( RelationalTypeMapping? typeMapping) => collateExpression.Update(ApplyTypeMapping(collateExpression.Operand, typeMapping)); - private SqlExpression ApplyTypeMappingOnDistinct( - DistinctExpression distinctExpression, - RelationalTypeMapping? typeMapping) - => distinctExpression.Update(ApplyTypeMapping(distinctExpression.Operand, typeMapping)); - private SqlExpression ApplyTypeMappingOnSqlUnary( SqlUnaryExpression sqlUnaryExpression, RelationalTypeMapping? typeMapping) @@ -223,6 +218,20 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( resultTypeMapping); } + private SqlExpression ApplyTypeMappingOnSqlEnumerable( + SqlEnumerableExpression sqlEnumerableExpression, RelationalTypeMapping? typeMapping) + { + var sqlExpression = ApplyTypeMapping(sqlEnumerableExpression.SqlExpression, typeMapping); + + var orderings = new List(); + foreach (var ordering in sqlEnumerableExpression.Orderings) + { + orderings.Add(ordering.Update(ApplyDefaultTypeMapping(ordering.Expression))); + } + + return sqlEnumerableExpression.Update(sqlExpression, orderings); + } + private SqlExpression ApplyTypeMappingOnIn(InExpression inExpression) { var itemTypeMapping = (inExpression.Values != null diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index 765d865c33b..63a6804a7f1 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -39,9 +39,6 @@ protected override Expression VisitExtension(Expression extensionExpression) case CrossJoinExpression crossJoinExpression: return VisitCrossJoin(crossJoinExpression); - case DistinctExpression distinctExpression: - return VisitDistinct(distinctExpression); - case ExceptExpression exceptExpression: return VisitExcept(exceptExpression); @@ -81,18 +78,21 @@ protected override Expression VisitExtension(Expression extensionExpression) case RowNumberExpression rowNumberExpression: return VisitRowNumber(rowNumberExpression); + case ScalarSubqueryExpression scalarSubqueryExpression: + return VisitScalarSubquery(scalarSubqueryExpression); + case SelectExpression selectExpression: return VisitSelect(selectExpression); case SqlBinaryExpression sqlBinaryExpression: return VisitSqlBinary(sqlBinaryExpression); - case SqlUnaryExpression sqlUnaryExpression: - return VisitSqlUnary(sqlUnaryExpression); - case SqlConstantExpression sqlConstantExpression: return VisitSqlConstant(sqlConstantExpression); + case SqlEnumerableExpression sqlEnumerableExpression: + return VisitSqlEnumerable(sqlEnumerableExpression); + case SqlFragmentExpression sqlFragmentExpression: return VisitSqlFragment(sqlFragmentExpression); @@ -102,8 +102,8 @@ protected override Expression VisitExtension(Expression extensionExpression) case SqlParameterExpression sqlParameterExpression: return VisitSqlParameter(sqlParameterExpression); - case ScalarSubqueryExpression scalarSubqueryExpression: - return VisitScalarSubquery(scalarSubqueryExpression); + case SqlUnaryExpression sqlUnaryExpression: + return VisitSqlUnary(sqlUnaryExpression); case TableExpression tableExpression: return VisitTable(tableExpression); @@ -150,13 +150,6 @@ protected override Expression VisitExtension(Expression extensionExpression) /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitCrossJoin(CrossJoinExpression crossJoinExpression); - /// - /// Visits the children of the distinct expression. - /// - /// The expression to visit. - /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. - protected abstract Expression VisitDistinct(DistinctExpression distinctExpression); - /// /// Visits the children of the except expression. /// @@ -276,6 +269,13 @@ protected override Expression VisitExtension(Expression extensionExpression) /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression); + /// + /// Visits the children of the sql enumerable expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitSqlEnumerable(SqlEnumerableExpression sqlEnumerableExpression); + /// /// Visits the children of the sql fragent expression. /// diff --git a/src/EFCore.Relational/Query/SqlExpressions/DistinctExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/DistinctExpression.cs deleted file mode 100644 index 88164099af7..00000000000 --- a/src/EFCore.Relational/Query/SqlExpressions/DistinctExpression.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; - -/// -/// -/// An expression that represents a DISTINCT in a SQL tree. -/// -/// -/// This type is typically used by database providers (and other extensions). It is generally -/// not used in application code. -/// -/// -public class DistinctExpression : SqlExpression -{ - /// - /// Creates a new instance of the class. - /// - /// An expression on which DISTINCT is applied. - public DistinctExpression(SqlExpression operand) - : base(operand.Type, operand.TypeMapping) - { - Operand = operand; - } - - /// - /// The expression on which DISTINCT is applied. - /// - public virtual SqlExpression Operand { get; } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => Update((SqlExpression)visitor.Visit(Operand)); - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// This expression if no children changed, or an expression with the updated children. - public virtual DistinctExpression Update(SqlExpression operand) - => operand != Operand - ? new DistinctExpression(operand) - : this; - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Append("(DISTINCT "); - expressionPrinter.Visit(Operand); - expressionPrinter.Append(")"); - } - - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is DistinctExpression distinctExpression - && Equals(distinctExpression)); - - private bool Equals(DistinctExpression distinctExpression) - => base.Equals(distinctExpression) - && Operand.Equals(distinctExpression.Operand); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Operand); -} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index a7afc248320..6ed206a7fad 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -159,6 +159,8 @@ private sealed class SqlRemappingVisitor : ExpressionVisitor private readonly SelectExpression _subquery; private readonly TableReferenceExpression _tableReferenceExpression; private readonly Dictionary _mappings; + private readonly HashSet _correlatedTerms; + private bool _groupByDiscovery; public SqlRemappingVisitor( Dictionary mappings, @@ -168,15 +170,28 @@ public SqlRemappingVisitor( _subquery = subquery; _tableReferenceExpression = tableReferenceExpression; _mappings = mappings; + _groupByDiscovery = subquery._groupBy.Count > 0; + _correlatedTerms = new HashSet(ReferenceEqualityComparer.Instance); } [return: NotNullIfNotNull("sqlExpression")] public SqlExpression? Remap(SqlExpression? sqlExpression) => (SqlExpression?)Visit(sqlExpression); - [return: NotNullIfNotNull("sqlExpression")] - public SelectExpression? Remap(SelectExpression? sqlExpression) - => (SelectExpression?)Visit(sqlExpression); + [return: NotNullIfNotNull("selectExpression")] + public SelectExpression? Remap(SelectExpression? selectExpression) + { + var result = (SelectExpression?)Visit(selectExpression); + + if (_correlatedTerms.Count > 0) + { + new EnclosingTermFindingVisitor(_correlatedTerms).Visit(selectExpression); + _groupByDiscovery = false; + result = (SelectExpression?)Visit(selectExpression); + } + + return result; + } [return: NotNullIfNotNull("expression")] public override Expression? Visit(Expression? expression) @@ -188,15 +203,70 @@ when _mappings.TryGetValue(sqlExpression, out var outer): return outer; case ColumnExpression columnExpression - when _subquery.ContainsTableReference(columnExpression): - var outerColumn = _subquery.GenerateOuterColumn(_tableReferenceExpression, columnExpression); - _mappings[columnExpression] = outerColumn; + when _groupByDiscovery + && _subquery.ContainsTableReference(columnExpression): + _correlatedTerms.Add(columnExpression); + return columnExpression; + + case SqlExpression sqlExpression + when !_groupByDiscovery + && sqlExpression is not SqlConstantExpression or SqlParameterExpression + && _correlatedTerms.Contains(sqlExpression): + var outerColumn = _subquery.GenerateOuterColumn(_tableReferenceExpression, sqlExpression); + _mappings[sqlExpression] = outerColumn; return outerColumn; + case ColumnExpression columnExpression + when !_groupByDiscovery + && _subquery.ContainsTableReference(columnExpression): + var outerColumn1 = _subquery.GenerateOuterColumn(_tableReferenceExpression, columnExpression); + _mappings[columnExpression] = outerColumn1; + return outerColumn1; + default: return base.Visit(expression); } } + + private sealed class EnclosingTermFindingVisitor : ExpressionVisitor + { + private readonly HashSet _correlatedTerms; + private bool _doesNotContainLocalTerms; + + public EnclosingTermFindingVisitor(HashSet correlatedTerms) + { + _correlatedTerms = correlatedTerms; + _doesNotContainLocalTerms = true; + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + { + if (expression is SqlExpression sqlExpression) + { + if (_correlatedTerms.Contains(sqlExpression) + || sqlExpression is SqlConstantExpression or SqlParameterExpression) + { + _correlatedTerms.Add(sqlExpression); + return sqlExpression; + } + + var parentDoesNotContainLocalTerms = _doesNotContainLocalTerms; + _doesNotContainLocalTerms = sqlExpression is not ColumnExpression; + base.Visit(expression); + if (_doesNotContainLocalTerms) + { + _correlatedTerms.Add(sqlExpression); + } + + _doesNotContainLocalTerms = _doesNotContainLocalTerms && parentDoesNotContainLocalTerms; + + return expression; + } + + return base.Visit(expression); + } + } } private sealed class ColumnExpressionFindingExpressionVisitor : ExpressionVisitor @@ -759,7 +829,6 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor var newOrderings = selectExpression._orderings.Select(Visit).ToList(); var offset = (SqlExpression?)Visit(selectExpression.Offset); var limit = (SqlExpression?)Visit(selectExpression.Limit); - var groupingCorrelationPredicate = (SqlExpression?)Visit(selectExpression._groupingCorrelationPredicate); var newSelectExpression = new SelectExpression( selectExpression.Alias, newProjections, newTables, newTableReferences, newGroupBy, newOrderings, selectExpression.GetAnnotations()) @@ -772,9 +841,6 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor Tags = selectExpression.Tags, _usedAliases = selectExpression._usedAliases.ToHashSet(), _projectionMapping = newProjectionMappings, - _groupingCorrelationPredicate = groupingCorrelationPredicate, - _groupingParentSelectExpressionId = selectExpression._groupingParentSelectExpressionId, - _groupingParentSelectExpressionTableCount = selectExpression._groupingParentSelectExpressionTableCount, }; newSelectExpression._mutable = selectExpression._mutable; @@ -825,198 +891,4 @@ public ColumnExpressionReplacingExpressionVisitor( concreteColumnExpression.IsNullable) : 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 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 - && subquery._groupingParentSelectExpressionId == _selectExpression._groupingParentSelectExpressionId - && subquery.Predicate.Equals(subquery._groupingCorrelationPredicate)) - - { - var initialTableCounts = 0; - initialTableCounts = _selectExpression._groupingParentSelectExpressionTableCount!.Value; - var potentialTableCount = Math.Min(_selectExpression._tables.Count, subquery._tables.Count); - // First verify that subquery has same structure for initial tables, - // If not then subquery may have different root than grouping element. - for (var i = 0; i < initialTableCounts; i++) - { - if (!string.Equals( - _selectExpression._tableReferences[i].Alias, - subquery._tableReferences[i].Alias, StringComparison.OrdinalIgnoreCase)) - { - initialTableCounts = 0; - break; - } - } - - if (initialTableCounts > 0) - { - // If initial table structure matches and - // Parent has additional joins lifted already one of them is a subquery join - // Then we abort lifting if any of the joins from the subquery to lift are a subquery join - if (_selectExpression._tables.Skip(initialTableCounts) - .Select(e => UnwrapJoinExpression(e)) - .Any(e => e is SelectExpression)) - { - for (var i = initialTableCounts; i < subquery._tables.Count; i++) - { - if (UnwrapJoinExpression(subquery._tables[i]) is SelectExpression) - { - // If any of the join is to subquery then we abort the lifting group by term altogether. - initialTableCounts = 0; - break; - } - } - } - } - - if (initialTableCounts > 0) - { - // We need to copy over owned join which are coming from same initial tables. - for (var i = 0; i < initialTableCounts; i++) - { - if (_selectExpression._tables[i] is SelectExpression originalNestedSelectExpression - && subquery._tables[i] is SelectExpression subqueryNestedSelectExpression) - { - CopyOverOwnedJoinInSameTable(originalNestedSelectExpression, subqueryNestedSelectExpression); - } - } - - - for (var i = initialTableCounts; i < potentialTableCount; i++) - { - // Try to match additional tables for the cases where we can match exact so we can avoid lifting - // same joins to parent - if (!string.Equals( - _selectExpression._tableReferences[i].Alias, - subquery._tableReferences[i].Alias, StringComparison.OrdinalIgnoreCase)) - { - break; - } - - var outerTableExpressionBase = _selectExpression._tables[i]; - var innerTableExpressionBase = subquery._tables[i]; - - if (outerTableExpressionBase is InnerJoinExpression outerInnerJoin - && innerTableExpressionBase is InnerJoinExpression innerInnerJoin) - { - outerTableExpressionBase = outerInnerJoin.Table as TableExpression; - innerTableExpressionBase = innerInnerJoin.Table as TableExpression; - } - else if (outerTableExpressionBase is LeftJoinExpression outerLeftJoin - && innerTableExpressionBase is LeftJoinExpression innerLeftJoin) - { - outerTableExpressionBase = outerLeftJoin.Table as TableExpression; - innerTableExpressionBase = innerLeftJoin.Table as TableExpression; - } - - if (outerTableExpressionBase is TableExpression outerTable - && innerTableExpressionBase is TableExpression innerTable - && !(string.Equals(outerTable.Name, innerTable.Name, StringComparison.OrdinalIgnoreCase) - && string.Equals(outerTable.Schema, innerTable.Schema, StringComparison.OrdinalIgnoreCase))) - { - break; - } - - initialTableCounts++; - } - } - - if (initialTableCounts > 0) - { - // If there are no initial table then this is not correlated grouping subquery - // We only replace columns from initial tables. - // Additional tables may have been added to outer from other terms which may end up matching on table alias - var columnExpressionReplacingExpressionVisitor = - new ColumnExpressionReplacingExpressionVisitor( - subquery, _selectExpression._tableReferences.Take(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; - } - } - } - - return base.Visit(expression); - } - - private static void CopyOverOwnedJoinInSameTable(SelectExpression target, SelectExpression source) - { - if (target._projection.Count != source._projection.Count) - { - var columnExpressionReplacingExpressionVisitor = new ColumnExpressionReplacingExpressionVisitor( - source, target._tableReferences); - 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 35efbfc23ea..726c326ead7 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -52,10 +52,6 @@ public sealed partial class SelectExpression : TableExpressionBase private Dictionary _projectionMapping = new(); private List _clientProjections = new(); private readonly List _aliasForClientProjections = new(); - - private SqlExpression? _groupingCorrelationPredicate; - private Guid? _groupingParentSelectExpressionId; - private int? _groupingParentSelectExpressionTableCount; private CloningExpressionVisitor? _cloningExpressionVisitor; private SelectExpression( @@ -590,7 +586,6 @@ static void UpdateLimit(SelectExpression selectExpression) || shapedQueryExpression.ResultCardinality == ResultCardinality.SingleOrDefault: { var innerSelectExpression = (SelectExpression)shapedQueryExpression.QueryExpression; - innerSelectExpression._groupingCorrelationPredicate = null; var innerShaperExpression = shapedQueryExpression.ShaperExpression; if (innerSelectExpression._clientProjections.Count == 0) { @@ -661,7 +656,6 @@ static Expression RemoveConvert(Expression expression) when shapedQueryExpression.ResultCardinality == ResultCardinality.Enumerable: { var innerSelectExpression = (SelectExpression)shapedQueryExpression.QueryExpression; - innerSelectExpression._groupingCorrelationPredicate = null; if (_identifier.Count == 0 || innerSelectExpression._identifier.Count == 0) { @@ -1128,8 +1122,6 @@ 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) { @@ -1182,7 +1174,6 @@ public void ApplyPredicate(SqlExpression sqlExpression) sqlExpression = PushdownIntoSubqueryInternal().Remap(sqlExpression); } - sqlExpression = TryLiftGroupByAggregate(sqlExpression); sqlExpression = AssignUniqueAliases(sqlExpression); if (_groupBy.Count > 0) @@ -1283,7 +1274,7 @@ public GroupByShaperExpression ApplyGrouping( var groupByAliases = new List(); PopulateGroupByTerms(keySelectorToAdd, groupByTerms, groupByAliases, "Key"); - if (groupByTerms.Any(e => e is SqlConstantExpression || e is SqlParameterExpression || e is ScalarSubqueryExpression)) + if (groupByTerms.Any(e => e is not ColumnExpression)) { // emptyKey will always hit this path. var sqlRemappingVisitor = PushdownIntoSubqueryInternal(); @@ -1310,18 +1301,12 @@ public GroupByShaperExpression ApplyGrouping( _groupBy.AddRange(groupByTerms); - // We generate the cloned expression before changing identifier for this SelectExpression - // because we are going to erase grouping for cloned expression. - _groupingParentSelectExpressionId = Guid.NewGuid(); - var clonedSelectExpression = Clone(); var correlationPredicate = groupByTerms.Zip(clonedSelectExpression._groupBy) .Select(e => sqlExpressionFactory.Equal(e.First, e.Second)) .Aggregate((l, r) => sqlExpressionFactory.AndAlso(l, r)); clonedSelectExpression._groupBy.Clear(); clonedSelectExpression.ApplyPredicate(correlationPredicate); - clonedSelectExpression._groupingCorrelationPredicate = clonedSelectExpression.Predicate; - _groupingParentSelectExpressionTableCount = _tables.Count; if (!_identifier.All(e => _groupBy.Contains(e.Column))) { @@ -1334,6 +1319,7 @@ public GroupByShaperExpression ApplyGrouping( return new GroupByShaperExpression( keySelector, + shaperExpression, new ShapedQueryExpression( clonedSelectExpression, new QueryExpressionReplacingExpressionVisitor(this, clonedSelectExpression).Visit(shaperExpression))); @@ -1404,12 +1390,6 @@ public void ApplyOrdering(OrderingExpression orderingExpression) /// An ordering expression to use for ordering. public void AppendOrdering(OrderingExpression orderingExpression) { - if (_groupBy.Count > 0) - { - orderingExpression = orderingExpression.Update( - (SqlExpression)new GroupByAggregateLiftingExpressionVisitor(this).Visit(orderingExpression.Expression)); - } - if (!_orderings.Any(o => o.Expression.Equals(orderingExpression.Expression))) { AppendOrderingInternal(orderingExpression); @@ -1522,18 +1502,12 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi Having = Having, Offset = Offset, Limit = Limit, - _groupingParentSelectExpressionId = _groupingParentSelectExpressionId, - _groupingParentSelectExpressionTableCount = _groupingParentSelectExpressionTableCount, - _groupingCorrelationPredicate = _groupingCorrelationPredicate }; Offset = null; Limit = null; IsDistinct = false; Predicate = null; Having = null; - _groupingCorrelationPredicate = null; - _groupingParentSelectExpressionId = null; - _groupingParentSelectExpressionTableCount = null; _groupBy.Clear(); _orderings.Clear(); _tables.Clear(); @@ -1618,9 +1592,6 @@ 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 uniquified // Right side is unique already when we did it when running select2 through it. innerColumn1 = (SqlExpression)aliasUniquifier.Visit(innerColumn1); @@ -2713,8 +2684,6 @@ private SqlRemappingVisitor PushdownIntoSubqueryInternal() Having = Having, Offset = Offset, Limit = Limit, - _groupingParentSelectExpressionId = _groupingParentSelectExpressionId, - _groupingParentSelectExpressionTableCount = _groupingParentSelectExpressionTableCount }; subquery._usedAliases = _usedAliases; subquery._mutable = false; @@ -2894,7 +2863,7 @@ private SqlRemappingVisitor PushdownIntoSubqueryInternal() if (_clientProjections[i] is ShapedQueryExpression shapedQueryExpression) { _clientProjections[i] = shapedQueryExpression.UpdateQueryExpression( - sqlRemappingVisitor.Visit(shapedQueryExpression.QueryExpression)); + sqlRemappingVisitor.Remap((SelectExpression)shapedQueryExpression.QueryExpression)); } } } @@ -3150,11 +3119,6 @@ private bool ContainsTableReference(ColumnExpression column) // At that point aliases are not uniquified 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) { Check.DebugAssert(_tables.Count == _tableReferences.Count, "All the tables should have their associated TableReferences."); @@ -3259,7 +3223,6 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) Offset = (SqlExpression?)visitor.Visit(Offset); Limit = (SqlExpression?)visitor.Visit(Limit); - _groupingCorrelationPredicate = (SqlExpression?)visitor.Visit(_groupingCorrelationPredicate); var identifier = VisitList(_identifier.Select(e => e.Column).ToList(), inPlace: true, out _) .Zip(_identifier, (a, b) => (a, b.Comparer)) @@ -3338,9 +3301,6 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) var limit = (SqlExpression?)visitor.Visit(Limit); changed |= limit != Limit; - var groupingCorrelationPredicate = (SqlExpression?)visitor.Visit(_groupingCorrelationPredicate); - changed |= groupingCorrelationPredicate != _groupingCorrelationPredicate; - var identifier = VisitList(_identifier.Select(e => e.Column).ToList(), inPlace: false, out var identifierChanged); changed |= identifierChanged; @@ -3363,9 +3323,6 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) IsDistinct = IsDistinct, Tags = Tags, _usedAliases = _usedAliases, - _groupingCorrelationPredicate = groupingCorrelationPredicate, - _groupingParentSelectExpressionId = _groupingParentSelectExpressionId, - _groupingParentSelectExpressionTableCount = _groupingParentSelectExpressionTableCount, }; newSelectExpression._mutable = false; newSelectExpression._tptLeftJoinTables.AddRange(_tptLeftJoinTables); diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlEnumerableExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlEnumerableExpression.cs new file mode 100644 index 00000000000..0ecd34085b2 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlEnumerableExpression.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// +/// An expression that represents an enumerable or group in a SQL tree. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class SqlEnumerableExpression : SqlExpression +{ + /// + /// Creates a new instance of the class. + /// + /// The underlying sql expression being enumerated. + /// A value indicating if distinct operator is applied on the enumerable or not. + /// A list of orderings to be applied to the enumerable. + public SqlEnumerableExpression(SqlExpression sqlExpression, bool distinct, IReadOnlyList? orderings) + : base(sqlExpression.Type, sqlExpression.TypeMapping) + { + SqlExpression = sqlExpression; + IsDistinct = distinct; + Orderings = orderings ?? Array.Empty(); + } + + /// + /// The underlying sql expression being enumerated. + /// + public virtual SqlExpression SqlExpression { get; } + + /// + /// The value indicating if distinct operator is applied on the enumerable or not. + /// + public virtual bool IsDistinct { get; } + + /// + /// The list of orderings to be applied to the enumerable. + /// + public virtual IReadOnlyList Orderings { get; } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var sqlExpression = (SqlExpression)visitor.Visit(SqlExpression); + var orderings = Orderings.Select(e => (OrderingExpression)visitor.Visit(e)).ToList(); + + return Update(sqlExpression, orderings); + } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual SqlEnumerableExpression Update(SqlExpression sqlExpression, IReadOnlyList orderings) + => sqlExpression != SqlExpression || !orderings.SequenceEqual(Orderings) + ? new SqlEnumerableExpression(sqlExpression, IsDistinct, orderings) + : this; + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + if (IsDistinct) + { + expressionPrinter.Append("DISTINCT ("); + } + + expressionPrinter.Visit(SqlExpression); + + if (IsDistinct) + { + expressionPrinter.Append(")"); + } + + if (Orderings.Count > 0) + { + expressionPrinter.Append(" ORDER BY "); + foreach (var ordering in Orderings) + { + expressionPrinter.Visit(ordering); + } + } + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SqlEnumerableExpression sqlEnumerableExpression + && Equals(sqlEnumerableExpression)); + + private bool Equals(SqlEnumerableExpression sqlEnumerableExpression) + => base.Equals(sqlEnumerableExpression) + && IsDistinct == sqlEnumerableExpression.IsDistinct + && SqlExpression.Equals(sqlEnumerableExpression.SqlExpression) + && Orderings.SequenceEqual(sqlEnumerableExpression.Orderings); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + hash.Add(IsDistinct); + hash.Add(SqlExpression); + foreach (var ordering in Orderings) + { + hash.Add(ordering); + } + + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlExpression.cs index 6a1c0b2203a..67dafe5cf56 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlExpression.cs @@ -12,6 +12,9 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; /// not used in application code. /// /// +#if DEBUG +[DebuggerDisplay("{new ExpressionPrinter().Print(this), nq}")] +#endif public abstract class SqlExpression : Expression, IPrintableExpression { /// diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 206454bba90..82d2b216aa3 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -354,8 +354,6 @@ CollateExpression collateExpression => VisitCollate(collateExpression, allowOptimizedExpansion, out nullable), ColumnExpression columnExpression => VisitColumn(columnExpression, allowOptimizedExpansion, out nullable), - DistinctExpression distinctExpression - => VisitDistinct(distinctExpression, allowOptimizedExpansion, out nullable), ExistsExpression existsExpression => VisitExists(existsExpression, allowOptimizedExpansion, out nullable), InExpression inExpression @@ -370,6 +368,8 @@ SqlBinaryExpression sqlBinaryExpression => VisitSqlBinary(sqlBinaryExpression, allowOptimizedExpansion, out nullable), SqlConstantExpression sqlConstantExpression => VisitSqlConstant(sqlConstantExpression, allowOptimizedExpansion, out nullable), + SqlEnumerableExpression sqlEnumerableExpression + => VisitSqlEnumerable(sqlEnumerableExpression, allowOptimizedExpansion, out nullable), SqlFragmentExpression sqlFragmentExpression => VisitSqlFragment(sqlFragmentExpression, allowOptimizedExpansion, out nullable), SqlFunctionExpression sqlFunctionExpression @@ -516,19 +516,6 @@ protected virtual SqlExpression VisitColumn( return columnExpression; } - /// - /// Visits a and computes its nullability. - /// - /// A collate expression to visit. - /// A bool value indicating if optimized expansion which considers null value as false value is allowed. - /// A bool value indicating whether the sql expression is nullable. - /// An optimized sql expression. - protected virtual SqlExpression VisitDistinct( - DistinctExpression distinctExpression, - bool allowOptimizedExpansion, - out bool nullable) - => distinctExpression.Update(Visit(distinctExpression.Operand, out nullable)); - /// /// Visits an and computes its nullability. /// @@ -955,6 +942,34 @@ protected virtual SqlExpression VisitSqlConstant( return sqlConstantExpression; } + /// + /// Visits a and computes its nullability. + /// + /// A sql enumerable expression to visit. + /// A bool value indicating if optimized expansion which considers null value as false value is allowed. + /// A bool value indicating whether the sql expression is nullable. + /// An optimized sql expression. + protected virtual SqlExpression VisitSqlEnumerable( + SqlEnumerableExpression sqlEnumerableExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + var sqlExpression = Visit(sqlEnumerableExpression.SqlExpression, out nullable); + var changed = sqlExpression != sqlEnumerableExpression.SqlExpression; + + var orderings = new List(); + foreach (var ordering in sqlEnumerableExpression.Orderings) + { + var newOrdering = ordering.Update(Visit(ordering.Expression, out _)); + changed |= newOrdering != ordering; + orderings.Add(newOrdering); + } + + return changed + ? sqlEnumerableExpression.Update(sqlExpression, orderings) + : sqlEnumerableExpression; + } + /// /// Visits a and computes its nullability. /// diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index da1fe7c7729..04710b864bc 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -160,22 +160,6 @@ protected override Expression VisitCollate(CollateExpression collateExpression) protected override Expression VisitColumn(ColumnExpression columnExpression) => ApplyConversion(columnExpression, condition: false); - /// - /// 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 VisitDistinct(DistinctExpression distinctExpression) - { - var parentSearchCondition = _isSearchCondition; - _isSearchCondition = false; - var operand = (SqlExpression)Visit(distinctExpression.Operand); - _isSearchCondition = parentSearchCondition; - - return ApplyConversion(distinctExpression.Update(operand), condition: false); - } - /// /// 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 @@ -408,6 +392,34 @@ protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpressio protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) => ApplyConversion(sqlConstantExpression, condition: false); + /// + /// 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 VisitSqlEnumerable(SqlEnumerableExpression sqlEnumerableExpression) + { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + var sqlExpression = (SqlExpression)Visit(sqlEnumerableExpression.SqlExpression); + var changed = sqlExpression != sqlEnumerableExpression.SqlExpression; + + var orderings = new List(); + foreach (var ordering in sqlEnumerableExpression.Orderings) + { + var orderingExpression = (SqlExpression)Visit(ordering.Expression); + changed |= orderingExpression != ordering.Expression; + orderings.Add(ordering.Update(orderingExpression)); + } + + _isSearchCondition = parentSearchCondition; + + return changed + ? sqlEnumerableExpression.Update(sqlExpression, orderings) + : sqlEnumerableExpression; + } + /// /// 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 @@ -506,8 +518,13 @@ protected override Expression VisitProjection(ProjectionExpression projectionExp /// protected override Expression VisitOrdering(OrderingExpression orderingExpression) { + var parentSearchCondition = _isSearchCondition; + _isSearchCondition = false; + var expression = (SqlExpression)Visit(orderingExpression.Expression); + _isSearchCondition = parentSearchCondition; + return orderingExpression.Update(expression); } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index 86d970b09fb..9997c1543d1 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -130,11 +130,11 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) /// 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 override SqlExpression? TranslateLongCount(SqlExpression sqlExpression) + public override SqlExpression? TranslateLongCount(SqlEnumerableExpression sqlEnumerableExpression) => Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping( Dependencies.SqlExpressionFactory.Function( "COUNT_BIG", - new[] { sqlExpression }, + new[] { sqlEnumerableExpression.Update(sqlEnumerableExpression.SqlExpression, Array.Empty()) }, nullable: false, argumentsPropagateNullability: new[] { false }, typeof(long))); diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs index dde75d7154d..671e965b0b0 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs @@ -218,9 +218,9 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) /// 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 override SqlExpression? TranslateAverage(SqlExpression sqlExpression) + public override SqlExpression? TranslateAverage(SqlEnumerableExpression sqlEnumerableExpression) { - var visitedExpression = base.TranslateAverage(sqlExpression); + var visitedExpression = base.TranslateAverage(sqlEnumerableExpression); var argumentType = GetProviderType(visitedExpression); if (argumentType == typeof(decimal)) { @@ -237,9 +237,9 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) /// 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 override SqlExpression? TranslateMax(SqlExpression sqlExpression) + public override SqlExpression? TranslateMax(SqlEnumerableExpression sqlEnumerableExpression) { - var visitedExpression = base.TranslateMax(sqlExpression); + var visitedExpression = base.TranslateMax(sqlEnumerableExpression); var argumentType = GetProviderType(visitedExpression); if (argumentType == typeof(DateTimeOffset) || argumentType == typeof(decimal) @@ -259,9 +259,9 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) /// 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 override SqlExpression? TranslateMin(SqlExpression sqlExpression) + public override SqlExpression? TranslateMin(SqlEnumerableExpression sqlEnumerableExpression) { - var visitedExpression = base.TranslateMin(sqlExpression); + var visitedExpression = base.TranslateMin(sqlEnumerableExpression); var argumentType = GetProviderType(visitedExpression); if (argumentType == typeof(DateTimeOffset) || argumentType == typeof(decimal) @@ -281,9 +281,9 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) /// 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 override SqlExpression? TranslateSum(SqlExpression sqlExpression) + public override SqlExpression? TranslateSum(SqlEnumerableExpression sqlEnumerableExpression) { - var visitedExpression = base.TranslateSum(sqlExpression); + var visitedExpression = base.TranslateSum(sqlEnumerableExpression); var argumentType = GetProviderType(visitedExpression); if (argumentType == typeof(decimal)) { diff --git a/src/EFCore/Query/GroupByShaperExpression.cs b/src/EFCore/Query/GroupByShaperExpression.cs index 8276de18b49..078791d5532 100644 --- a/src/EFCore/Query/GroupByShaperExpression.cs +++ b/src/EFCore/Query/GroupByShaperExpression.cs @@ -21,23 +21,31 @@ 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. + /// An expression representing key selector for the grouping result. + /// An expression representing element selector for the grouping result. + /// An expression representing subquery for enumerable over the grouping result. public GroupByShaperExpression( Expression keySelector, + Expression elementSelector, ShapedQueryExpression groupingEnumerable) { KeySelector = keySelector; + ElementSelector = elementSelector; GroupingEnumerable = groupingEnumerable; } /// - /// The expression representing the key selector for this grouping element. + /// The expression representing the key selector for this grouping result. /// public virtual Expression KeySelector { get; } /// - /// The expression representing the element selector for this grouping element. + /// The expression representing the element selector for this grouping result. + /// + public virtual Expression ElementSelector { get; } + + /// + /// The expression representing the subquery for the enumerable over this grouping result. /// public virtual ShapedQueryExpression GroupingEnumerable { get; } @@ -51,24 +59,8 @@ public sealed override ExpressionType NodeType /// protected override Expression VisitChildren(ExpressionVisitor visitor) - { - var keySelector = visitor.Visit(KeySelector); - var groupingEnumerable = (ShapedQueryExpression)visitor.Visit(GroupingEnumerable); - - return Update(keySelector, groupingEnumerable); - } - - /// - /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will - /// return this expression. - /// - /// The property of the result. - /// The property of the result. - /// This expression if no children changed, or an expression with the updated children. - public virtual GroupByShaperExpression Update(Expression keySelector, ShapedQueryExpression groupingEnumerable) - => keySelector != KeySelector || groupingEnumerable != GroupingEnumerable - ? new GroupByShaperExpression(keySelector, groupingEnumerable) - : this; + => throw new InvalidOperationException( + CoreStrings.VisitIsNotAllowed($"{nameof(GroupByShaperExpression)}.{nameof(VisitChildren)}")); /// void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) @@ -77,6 +69,9 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) expressionPrinter.Append("KeySelector: "); expressionPrinter.Visit(KeySelector); expressionPrinter.AppendLine(", "); + expressionPrinter.Append("ElementSelector: "); + expressionPrinter.Visit(ElementSelector); + expressionPrinter.AppendLine(", "); expressionPrinter.Append("GroupingEnumerable:"); expressionPrinter.Visit(GroupingEnumerable); expressionPrinter.AppendLine(); diff --git a/src/EFCore/Query/ReplacingExpressionVisitor.cs b/src/EFCore/Query/ReplacingExpressionVisitor.cs index 068201ea63a..ef98fb944c4 100644 --- a/src/EFCore/Query/ReplacingExpressionVisitor.cs +++ b/src/EFCore/Query/ReplacingExpressionVisitor.cs @@ -50,7 +50,8 @@ public ReplacingExpressionVisitor(IReadOnlyList originals, IReadOnly { if (expression == null || expression is ShapedQueryExpression - || expression is EntityShaperExpression) + || expression is EntityShaperExpression + || expression is GroupByShaperExpression) { return expression; } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs index bed24e8bace..456a34a15d6 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs @@ -29,14 +29,6 @@ public override async Task .Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex( async))).Message); - public override async Task Group_by_on_StartsWith_with_null_parameter_as_argument(bool async) - // Grouping by constant. Issue #19683. - => Assert.Equal( - "1", - (await Assert.ThrowsAsync( - () => base.Group_by_on_StartsWith_with_null_parameter_as_argument(async))) - .Actual); - public override async Task Projecting_entity_as_well_as_correlated_collection_followed_by_Distinct(bool async) // Distinct. Issue #24325. => Assert.Equal( diff --git a/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs index e6845562cf2..7884025819f 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs @@ -24,16 +24,6 @@ public virtual Task Parameter_used_multiple_times_take_appropriate_inferred_type ss => ss.Set().Where(e => e.Nation == place || e.Location == place)); } - public override async Task - Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( - bool async) - => Assert.Equal( - RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, - (await Assert.ThrowsAsync( - () => base - .Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( - async))).Message); - public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column_also_projecting_complex_expressions( bool async) => Assert.Equal( diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs index 0c6823934db..fa78a242354 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs @@ -11,24 +11,6 @@ protected NorthwindGroupByQueryRelationalTestBase(TFixture fixture) { } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public override async Task Complex_query_with_groupBy_in_subquery4(bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.Complex_query_with_groupBy_in_subquery4(async))).Message; - - Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, message); - } - - public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(async))).Message; - - Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, message); - } - protected virtual bool CanExecuteQueryString => false; diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs index 6a91d8d3b39..97620c0a513 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs @@ -11,15 +11,6 @@ protected NorthwindSelectQueryRelationalTestBase(TFixture fixture) { } - public override async Task Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier( - bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier(async))).Message; - - Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, message); - } - public override Task Select_bool_closure_with_order_by_property_with_cast_to_nullable(bool async) => AssertTranslationFailed(() => base.Select_bool_closure_with_order_by_property_with_cast_to_nullable(async)); diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 471f9a97ebb..6785b2ab707 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -5929,7 +5929,7 @@ public virtual Task Group_by_on_StartsWith_with_null_parameter_as_argument(bool return AssertQueryScalar( async, ss => ss.Set().GroupBy(g => g.FullName.StartsWith(prm)).Select(g => g.Key), - ss => ss.Set().Select(g => false)); + ss => ss.Set().GroupBy(g => false).Select(g => g.Key)); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index f7add221f6d..3632e44248c 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -1211,6 +1211,33 @@ public virtual Task Element_selector_with_case_block_repeated_inside_another_cas into g select new { g.Key.OrderID, Aggregate = g.Sum(s => s.IsAlfki ? s.OrderId : -s.OrderId) }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_conditional_properties(bool async) + { + var groupByMonth = false; + var groupByCustomer = true; + + return AssertQuery( + async, + ss => ss.Set() + .GroupBy( + x => new + { + OrderMonth = groupByMonth ? (int?)x.OrderDate.Value.Month : null, + Customer = groupByCustomer ? x.CustomerID : null + }, + x => x, + (key, items) => new { key.OrderMonth, key.Customer, Count = items.Count() }), + elementSorter: e => (e.OrderMonth, e.Customer), + elementAsserter: (e, a) => + { + Assert.Equal(e.OrderMonth, a.OrderMonth); + Assert.Equal(e.Customer, a.Customer); + Assert.Equal(e.Count, a.Count); + }); + } + #endregion #region GroupByAfterComposition @@ -1525,6 +1552,73 @@ public virtual Task GroupBy_after_anonymous_projection_and_distinct_followed_by_ Assert.Equal(e.Count, a.Count); }); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_complex_key_aggregate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.Customer.CustomerID.Substring(0, 1)) + .Select(g => new { Key = g.Key, Count = g.Count() }), + elementSorter: e => (e.Key, e.Count), + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + Assert.Equal(e.Count, a.Count); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_complex_key_aggregate_2(bool async) + => AssertQuery( + async, + ss => from s in (from o in ss.Set() + group o by o.OrderDate.Value.Month + into g + select new + { + Month = g.Key, + Total = g.Sum(e => e.OrderID) + }) + select new + { + s.Month, + s.Total, + Payment = ss.Set().Where(e => e.OrderDate.Value.Month == s.Month).Sum(e => e.OrderID) + }, + elementSorter: e => (e.Month, e.Total), + elementAsserter: (e, a) => + { + Assert.Equal(e.Month, a.Month); + Assert.Equal(e.Total, a.Total); + Assert.Equal(e.Payment, a.Payment); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_collection_of_scalar_before_GroupBy_aggregate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .Select(c => new + { + c.CustomerID, + c.City, + Orders = c.Orders.Select(e => e.OrderID) + }) + .GroupBy(e => e.City) + .Select(g => new + { + g.Key, + Count = g.Count() + }), + elementSorter: e => (e.Key, e.Count), + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + Assert.Equal(e.Count, a.Count); + }); + #endregion #region GroupByAggregateComposition @@ -2243,6 +2337,53 @@ from o in ss.Set() select o, entryCount: 89); + [ConditionalTheory(Skip = "Issue#27480")] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_aggregate_left_join_GroupBy_aggregate_left_join(bool async) + => AssertQuery( + async, + ss => from c1 in ss.Set() + from c2 in (from c in ss.Set() + from oc1 in ss.Set() + .GroupBy(o => o.CustomerID, (o, g) => new { CustomerID = o, Count = (int?)g.Count() }) + .Where(x => x.CustomerID == c.CustomerID).DefaultIfEmpty() + group new { c.CustomerID, oc1.Count } by c.CustomerID into g + select new + { + CustomerID = g.Key, + Count = g.Sum(x => x.Count) + }).Where(x => x.CustomerID == c1.CustomerID).DefaultIfEmpty() + select new + { + c1.CustomerID, + c1.City, + c2.Count + }, + ss => from c1 in ss.Set() + from c2 in (from c in ss.Set() + from oc1 in ss.Set() + .GroupBy(o => o.CustomerID, (o, g) => new { CustomerID = o, Count = (int?)g.Count() }) + .Where(x => x.CustomerID == c.CustomerID).DefaultIfEmpty() + group new { c.CustomerID, Count = oc1.MaybeScalar(e => e.Count) } by c.CustomerID into g + select new + { + CustomerID = g.Key, + Count = g.Sum(x => x.Count) + }).Where(x => x.CustomerID == c1.CustomerID).DefaultIfEmpty() + select new + { + c1.CustomerID, + c1.City, + c2.Count + }, + elementSorter: e => e.CustomerID, + elementAsserter: (e, a) => + { + Assert.Equal(e.CustomerID, a.CustomerID); + Assert.Equal(e.City, a.City); + Assert.Equal(e.Count, a.Count); + }); + #endregion #region GroupByAggregateChainComposition @@ -2512,6 +2653,35 @@ public virtual Task GroupBy_Distinct(bool async) async, ss => ss.Set().GroupBy(o => o.CustomerID).Distinct().Select(g => g.Key))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_complex_key_without_aggregate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => o.Customer.CustomerID.Substring(0, 1)) + .Select(g => new { Key = g.Key, Count = g.Skip(1).Take(2) }), + elementSorter: e => (e.Key, e.Count), + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection(e.Count, a.Count); + }, + entryCount: 42); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_selecting_grouping_key_list(bool async) + => AssertQuery( + async, + ss => ss.Set().GroupBy(o => o.CustomerID).Select(g => new { g.Key, Data = g.Select(e => e.CustomerID).ToList() }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection(e.Data, a.Data); + }); + #endregion #region GroupBySelectFirst @@ -2822,6 +2992,25 @@ public virtual Task GroupBy_Count_in_projection(bool async) HasMultipleProducts = info.OrderDetails.GroupBy(e => e.Product.ProductName).Count() > 1 })); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_nominal_type_count(bool async) + => AssertCount( + async, + ss => ss.Set() + .GroupBy(o => o.CustomerID) + .Select(e => new Result(e.Key))); + + private class Result + { + private readonly string _customerID; + + public Result(string customerID) + { + _customerID = customerID; + } + } + #endregion # region GroupByInSubquery @@ -2993,7 +3182,7 @@ public virtual Task AsEnumerable_in_subquery_for_GroupBy(bool async) }, entryCount: 15); - [ConditionalTheory] + [ConditionalTheory(Skip = "Issue#27130")] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_aggregate_from_multiple_query_in_same_projection(bool async) => AssertQuery( @@ -3025,7 +3214,7 @@ public virtual Task GroupBy_aggregate_from_multiple_query_in_same_projection_2(b }), elementSorter: e => e.Key); - [ConditionalTheory] + [ConditionalTheory(Skip = "Issue#27130")] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_aggregate_from_multiple_query_in_same_projection_3(bool async) => AssertQuery( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index cecaf0b5f98..1e42c6ae6ab 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -3397,9 +3397,14 @@ 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([l2].[Id], 0) + COALESCE([l2].[Id], 0)) > 0"); +HAVING ( + SELECT MIN(COALESCE([l5].[Id], 0) + COALESCE([l5].[Id], 0)) + FROM [LevelOne] AS [l2] + LEFT JOIN [LevelTwo] AS [l3] ON [l2].[Id] = [l3].[Id] + LEFT JOIN [LevelThree] AS [l4] ON [l3].[Id] = [l4].[Id] + LEFT JOIN [LevelTwo] AS [l5] ON [l2].[Id] = [l5].[Id] + WHERE [l1].[Name] = [l4].[Name] OR ([l1].[Name] IS NULL AND [l4].[Name] IS NULL)) > 0"); } public override async Task Nested_object_constructed_from_group_key_properties(bool async) @@ -3552,14 +3557,17 @@ public override async Task Composite_key_join_on_groupby_aggregate_projecting_on await base.Composite_key_join_on_groupby_aggregate_projecting_only_grouping_key(async); AssertSql( - @"SELECT [t].[Key] + @"SELECT [t0].[Key] FROM [LevelOne] AS [l] INNER JOIN ( - SELECT [l0].[Id] % 3 AS [Key], COALESCE(SUM([l0].[Id]), 0) AS [Sum] - FROM [LevelTwo] AS [l0] - GROUP BY [l0].[Id] % 3 -) AS [t] ON [l].[Id] = [t].[Key] AND CAST(1 AS bit) = CASE - WHEN [t].[Sum] > 10 THEN CAST(1 AS bit) + SELECT [t].[Key], COALESCE(SUM([t].[Id]), 0) AS [Sum] + FROM ( + SELECT [l0].[Id], [l0].[Id] % 3 AS [Key] + FROM [LevelTwo] AS [l0] + ) AS [t] + GROUP BY [t].[Key] +) AS [t0] ON [l].[Id] = [t0].[Key] AND CAST(1 AS bit) = CASE + WHEN [t0].[Sum] > 10 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } @@ -3842,9 +3850,14 @@ public override async Task Simple_level1_level2_GroupBy_Having_Count(bool async) 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([l2].[Id], 0)) > 0"); +HAVING ( + SELECT MIN(COALESCE([l5].[Id], 0)) + FROM [LevelOne] AS [l2] + LEFT JOIN [LevelTwo] AS [l3] ON [l2].[Id] = [l3].[Id] + LEFT JOIN [LevelThree] AS [l4] ON [l3].[Id] = [l4].[Id] + LEFT JOIN [LevelTwo] AS [l5] ON [l2].[Id] = [l5].[Id] + WHERE [l1].[Name] = [l4].[Name] OR ([l1].[Name] IS NULL AND [l4].[Name] IS NULL)) > 0"); } public override async Task Simple_level1_level2_level3_include(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index beeed8edc28..ed14f97620f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -142,18 +142,44 @@ WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS END = CASE WHEN [t0].[Level2_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [t0].[Id] END -LEFT JOIN ( - SELECT [l5].[Id], [l5].[OneToOne_Required_PK_Date], [l5].[Level1_Required_Id], [l5].[OneToMany_Required_Inverse2Id] - 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] = CASE - WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] -END GROUP BY [t0].[Level3_Name] -HAVING MIN(COALESCE(CASE - WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] -END, 0)) > 0"); +HAVING ( + SELECT MIN(COALESCE(CASE + WHEN [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t4].[Id] + END, 0)) + FROM [Level1] AS [l5] + LEFT JOIN ( + SELECT [l6].[Id], [l6].[OneToOne_Required_PK_Date], [l6].[Level1_Optional_Id], [l6].[Level1_Required_Id], [l6].[Level2_Name], [l6].[OneToMany_Optional_Inverse2Id], [l6].[OneToMany_Required_Inverse2Id], [l6].[OneToOne_Optional_PK_Inverse2Id], [l7].[Id] AS [Id0] + FROM [Level1] AS [l6] + INNER JOIN [Level1] AS [l7] ON [l6].[Id] = [l7].[Id] + WHERE [l6].[OneToOne_Required_PK_Date] IS NOT NULL AND [l6].[Level1_Required_Id] IS NOT NULL AND [l6].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t2] ON [l5].[Id] = CASE + WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] + END + LEFT JOIN ( + SELECT [l8].[Id], [l8].[Level2_Optional_Id], [l8].[Level2_Required_Id], [l8].[Level3_Name], [l8].[OneToMany_Optional_Inverse3Id], [l8].[OneToMany_Required_Inverse3Id], [l8].[OneToOne_Optional_PK_Inverse3Id], [t5].[Id] AS [Id0], [t5].[Id0] AS [Id00] + FROM [Level1] AS [l8] + INNER JOIN ( + SELECT [l9].[Id], [l9].[OneToOne_Required_PK_Date], [l9].[Level1_Optional_Id], [l9].[Level1_Required_Id], [l9].[Level2_Name], [l9].[OneToMany_Optional_Inverse2Id], [l9].[OneToMany_Required_Inverse2Id], [l9].[OneToOne_Optional_PK_Inverse2Id], [l10].[Id] AS [Id0] + FROM [Level1] AS [l9] + INNER JOIN [Level1] AS [l10] ON [l9].[Id] = [l10].[Id] + WHERE [l9].[OneToOne_Required_PK_Date] IS NOT NULL AND [l9].[Level1_Required_Id] IS NOT NULL AND [l9].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t5] ON [l8].[Id] = [t5].[Id] + WHERE [l8].[Level2_Required_Id] IS NOT NULL AND [l8].[OneToMany_Required_Inverse3Id] IS NOT NULL + ) AS [t3] ON CASE + WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] + END = CASE + WHEN [t3].[Level2_Required_Id] IS NOT NULL AND [t3].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [t3].[Id] + END + LEFT JOIN ( + SELECT [l11].[Id], [l11].[OneToOne_Required_PK_Date], [l11].[Level1_Optional_Id], [l11].[Level1_Required_Id], [l11].[Level2_Name], [l11].[OneToMany_Optional_Inverse2Id], [l11].[OneToMany_Required_Inverse2Id], [l11].[OneToOne_Optional_PK_Inverse2Id], [l12].[Id] AS [Id0] + FROM [Level1] AS [l11] + INNER JOIN [Level1] AS [l12] ON [l11].[Id] = [l12].[Id] + WHERE [l11].[OneToOne_Required_PK_Date] IS NOT NULL AND [l11].[Level1_Required_Id] IS NOT NULL AND [l11].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t4] ON [l5].[Id] = CASE + WHEN [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t4].[Id] + END + WHERE [t0].[Level3_Name] = [t3].[Level3_Name] OR ([t0].[Level3_Name] IS NULL AND [t3].[Level3_Name] IS NULL)) > 0"); } public override async Task Simple_level1_level2_level3_include(bool async) @@ -494,20 +520,46 @@ WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS END = CASE WHEN [t0].[Level2_Required_Id] IS NOT NULL AND [t0].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [t0].[Id] END -LEFT JOIN ( - SELECT [l5].[Id], [l5].[OneToOne_Required_PK_Date], [l5].[Level1_Required_Id], [l5].[OneToMany_Required_Inverse2Id] - 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] = CASE - WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] -END GROUP BY [t0].[Level3_Name] -HAVING MIN(COALESCE(CASE - WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] -END, 0) + COALESCE(CASE - WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] -END, 0)) > 0"); +HAVING ( + SELECT MIN(COALESCE(CASE + WHEN [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t4].[Id] + END, 0) + COALESCE(CASE + WHEN [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t4].[Id] + END, 0)) + FROM [Level1] AS [l5] + LEFT JOIN ( + SELECT [l6].[Id], [l6].[OneToOne_Required_PK_Date], [l6].[Level1_Optional_Id], [l6].[Level1_Required_Id], [l6].[Level2_Name], [l6].[OneToMany_Optional_Inverse2Id], [l6].[OneToMany_Required_Inverse2Id], [l6].[OneToOne_Optional_PK_Inverse2Id], [l7].[Id] AS [Id0] + FROM [Level1] AS [l6] + INNER JOIN [Level1] AS [l7] ON [l6].[Id] = [l7].[Id] + WHERE [l6].[OneToOne_Required_PK_Date] IS NOT NULL AND [l6].[Level1_Required_Id] IS NOT NULL AND [l6].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t2] ON [l5].[Id] = CASE + WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] + END + LEFT JOIN ( + SELECT [l8].[Id], [l8].[Level2_Optional_Id], [l8].[Level2_Required_Id], [l8].[Level3_Name], [l8].[OneToMany_Optional_Inverse3Id], [l8].[OneToMany_Required_Inverse3Id], [l8].[OneToOne_Optional_PK_Inverse3Id], [t5].[Id] AS [Id0], [t5].[Id0] AS [Id00] + FROM [Level1] AS [l8] + INNER JOIN ( + SELECT [l9].[Id], [l9].[OneToOne_Required_PK_Date], [l9].[Level1_Optional_Id], [l9].[Level1_Required_Id], [l9].[Level2_Name], [l9].[OneToMany_Optional_Inverse2Id], [l9].[OneToMany_Required_Inverse2Id], [l9].[OneToOne_Optional_PK_Inverse2Id], [l10].[Id] AS [Id0] + FROM [Level1] AS [l9] + INNER JOIN [Level1] AS [l10] ON [l9].[Id] = [l10].[Id] + WHERE [l9].[OneToOne_Required_PK_Date] IS NOT NULL AND [l9].[Level1_Required_Id] IS NOT NULL AND [l9].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t5] ON [l8].[Id] = [t5].[Id] + WHERE [l8].[Level2_Required_Id] IS NOT NULL AND [l8].[OneToMany_Required_Inverse3Id] IS NOT NULL + ) AS [t3] ON CASE + WHEN [t2].[OneToOne_Required_PK_Date] IS NOT NULL AND [t2].[Level1_Required_Id] IS NOT NULL AND [t2].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t2].[Id] + END = CASE + WHEN [t3].[Level2_Required_Id] IS NOT NULL AND [t3].[OneToMany_Required_Inverse3Id] IS NOT NULL THEN [t3].[Id] + END + LEFT JOIN ( + SELECT [l11].[Id], [l11].[OneToOne_Required_PK_Date], [l11].[Level1_Optional_Id], [l11].[Level1_Required_Id], [l11].[Level2_Name], [l11].[OneToMany_Optional_Inverse2Id], [l11].[OneToMany_Required_Inverse2Id], [l11].[OneToOne_Optional_PK_Inverse2Id], [l12].[Id] AS [Id0] + FROM [Level1] AS [l11] + INNER JOIN [Level1] AS [l12] ON [l11].[Id] = [l12].[Id] + WHERE [l11].[OneToOne_Required_PK_Date] IS NOT NULL AND [l11].[Level1_Required_Id] IS NOT NULL AND [l11].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t4] ON [l5].[Id] = CASE + WHEN [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t4].[Id] + END + WHERE [t0].[Level3_Name] = [t3].[Level3_Name] OR ([t0].[Level3_Name] IS NULL AND [t3].[Level3_Name] IS NULL)) > 0"); } public override async Task Sum_with_selector_cast_using_as(bool async) @@ -789,26 +841,27 @@ public override async Task Nested_object_constructed_from_group_key_properties(b await base.Nested_object_constructed_from_group_key_properties(async); AssertSql( - @"SELECT [l].[Id], [l].[Name], [l].[Date], CASE - WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] -END AS [Id], [t0].[Level2_Name] AS [Name], [t].[OneToOne_Required_PK_Date] AS [Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], COALESCE(SUM(CAST(LEN([l].[Name]) AS int)), 0) AS [Aggregate] -FROM [Level1] AS [l] -LEFT JOIN ( - SELECT [l0].[Id], [l0].[OneToOne_Required_PK_Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[OneToMany_Required_Inverse2Id] - FROM [Level1] AS [l0] - INNER JOIN [Level1] AS [l1] ON [l0].[Id] = [l1].[Id] - WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL -) AS [t] ON [l].[Id] = [t].[Level1_Optional_Id] -LEFT JOIN ( - SELECT [l2].[Level1_Required_Id], [l2].[Level2_Name] - FROM [Level1] AS [l2] - INNER JOIN [Level1] AS [l3] ON [l2].[Id] = [l3].[Id] - WHERE [l2].[OneToOne_Required_PK_Date] IS NOT NULL AND [l2].[Level1_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse2Id] IS NOT NULL -) AS [t0] ON [l].[Id] = [t0].[Level1_Required_Id] -WHERE [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL -GROUP BY [l].[Id], [l].[Date], [l].[Name], CASE - WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] -END, [t].[OneToOne_Required_PK_Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t0].[Level2_Name]"); + @"SELECT [t1].[Id], [t1].[Name], [t1].[Date], [t1].[InnerId] AS [Id], [t1].[Level2_Name0] AS [Name], [t1].[OneToOne_Required_PK_Date] AS [Date], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], COALESCE(SUM(CAST(LEN([t1].[Name]) AS int)), 0) AS [Aggregate] +FROM ( + SELECT [l].[Id], [l].[Date], [l].[Name], [t].[OneToOne_Required_PK_Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t0].[Level2_Name] AS [Level2_Name0], CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END AS [InnerId] + FROM [Level1] AS [l] + LEFT JOIN ( + SELECT [l0].[Id], [l0].[OneToOne_Required_PK_Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[OneToMany_Required_Inverse2Id] + FROM [Level1] AS [l0] + INNER JOIN [Level1] AS [l1] ON [l0].[Id] = [l1].[Id] + WHERE [l0].[OneToOne_Required_PK_Date] IS NOT NULL AND [l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t] ON [l].[Id] = [t].[Level1_Optional_Id] + LEFT JOIN ( + SELECT [l2].[Level1_Required_Id], [l2].[Level2_Name] + FROM [Level1] AS [l2] + INNER JOIN [Level1] AS [l3] ON [l2].[Id] = [l3].[Id] + WHERE [l2].[OneToOne_Required_PK_Date] IS NOT NULL AND [l2].[Level1_Required_Id] IS NOT NULL AND [l2].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t0] ON [l].[Id] = [t0].[Level1_Required_Id] + WHERE [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL +) AS [t1] +GROUP BY [t1].[Id], [t1].[Date], [t1].[Name], [t1].[InnerId], [t1].[OneToOne_Required_PK_Date], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], [t1].[Level2_Name0]"); } public override async Task Contains_over_optional_navigation_with_null_parameter(bool async) @@ -877,37 +930,55 @@ public override async Task Composite_key_join_on_groupby_aggregate_projecting_on await base.Composite_key_join_on_groupby_aggregate_projecting_only_grouping_key(async); AssertSql( - @"SELECT [t0].[Key] + @"SELECT [t1].[Key] FROM [Level1] AS [l] INNER JOIN ( - SELECT CASE - WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] - END % 3 AS [Key], COALESCE(SUM(CASE - WHEN [t1].[OneToOne_Required_PK_Date] IS NOT NULL AND [t1].[Level1_Required_Id] IS NOT NULL AND [t1].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t1].[Id] - END), 0) AS [Sum] - FROM [Level1] AS [l0] - LEFT JOIN ( - SELECT [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[Level1_Required_Id], [l1].[OneToMany_Required_Inverse2Id] - FROM [Level1] AS [l1] - INNER JOIN [Level1] AS [l2] ON [l1].[Id] = [l2].[Id] - WHERE [l1].[OneToOne_Required_PK_Date] IS NOT NULL AND [l1].[Level1_Required_Id] IS NOT NULL AND [l1].[OneToMany_Required_Inverse2Id] IS NOT NULL - ) AS [t] ON [l0].[Id] = CASE - WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] - END - LEFT JOIN ( - SELECT [l3].[Id], [l3].[OneToOne_Required_PK_Date], [l3].[Level1_Required_Id], [l3].[OneToMany_Required_Inverse2Id] - FROM [Level1] AS [l3] - INNER JOIN [Level1] AS [l4] ON [l3].[Id] = [l4].[Id] - WHERE [l3].[OneToOne_Required_PK_Date] IS NOT NULL AND [l3].[Level1_Required_Id] IS NOT NULL AND [l3].[OneToMany_Required_Inverse2Id] IS NOT NULL - ) AS [t1] ON [l0].[Id] = CASE - WHEN [t1].[OneToOne_Required_PK_Date] IS NOT NULL AND [t1].[Level1_Required_Id] IS NOT NULL AND [t1].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t1].[Id] - END - WHERE [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL - GROUP BY CASE - WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] - END % 3 -) AS [t0] ON [l].[Id] = [t0].[Key] AND CAST(1 AS bit) = CASE - WHEN [t0].[Sum] > 10 THEN CAST(1 AS bit) + SELECT [t0].[Key], ( + SELECT COALESCE(SUM(CASE + WHEN [t3].[OneToOne_Required_PK_Date] IS NOT NULL AND [t3].[Level1_Required_Id] IS NOT NULL AND [t3].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t3].[Id] + END), 0) + FROM ( + SELECT [l2].[Id], [l2].[Date], [l2].[Name], [t4].[Id] AS [Id0], [t4].[OneToOne_Required_PK_Date], [t4].[Level1_Optional_Id], [t4].[Level1_Required_Id], [t4].[Level2_Name], [t4].[OneToMany_Optional_Inverse2Id], [t4].[OneToMany_Required_Inverse2Id], [t4].[OneToOne_Optional_PK_Inverse2Id], [t4].[Id0] AS [Id00], CASE + WHEN [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t4].[Id] + END % 3 AS [Key] + FROM [Level1] AS [l2] + LEFT JOIN ( + SELECT [l3].[Id], [l3].[OneToOne_Required_PK_Date], [l3].[Level1_Optional_Id], [l3].[Level1_Required_Id], [l3].[Level2_Name], [l3].[OneToMany_Optional_Inverse2Id], [l3].[OneToMany_Required_Inverse2Id], [l3].[OneToOne_Optional_PK_Inverse2Id], [l4].[Id] AS [Id0] + FROM [Level1] AS [l3] + INNER JOIN [Level1] AS [l4] ON [l3].[Id] = [l4].[Id] + WHERE [l3].[OneToOne_Required_PK_Date] IS NOT NULL AND [l3].[Level1_Required_Id] IS NOT NULL AND [l3].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t4] ON [l2].[Id] = CASE + WHEN [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t4].[Id] + END + WHERE [t4].[OneToOne_Required_PK_Date] IS NOT NULL AND [t4].[Level1_Required_Id] IS NOT NULL AND [t4].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t2] + LEFT JOIN ( + SELECT [l5].[Id], [l5].[OneToOne_Required_PK_Date], [l5].[Level1_Optional_Id], [l5].[Level1_Required_Id], [l5].[Level2_Name], [l5].[OneToMany_Optional_Inverse2Id], [l5].[OneToMany_Required_Inverse2Id], [l5].[OneToOne_Optional_PK_Inverse2Id], [l6].[Id] AS [Id0] + 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 [t3] ON [t2].[Id] = CASE + WHEN [t3].[OneToOne_Required_PK_Date] IS NOT NULL AND [t3].[Level1_Required_Id] IS NOT NULL AND [t3].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t3].[Id] + END + WHERE [t0].[Key] = [t2].[Key] OR ([t0].[Key] IS NULL AND [t2].[Key] IS NULL)) AS [Sum] + FROM ( + SELECT CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END % 3 AS [Key] + FROM [Level1] AS [l0] + LEFT JOIN ( + SELECT [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[Level1_Required_Id], [l1].[OneToMany_Required_Inverse2Id] + FROM [Level1] AS [l1] + INNER JOIN [Level1] AS [l7] ON [l1].[Id] = [l7].[Id] + WHERE [l1].[OneToOne_Required_PK_Date] IS NOT NULL AND [l1].[Level1_Required_Id] IS NOT NULL AND [l1].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t] ON [l0].[Id] = CASE + WHEN [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL THEN [t].[Id] + END + WHERE [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL + ) AS [t0] + GROUP BY [t0].[Key] +) AS [t1] ON [l].[Id] = [t1].[Key] AND CAST(1 AS bit) = CASE + WHEN [t1].[Sum] > 10 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs index 1cd5c731557..8418b4d3370 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs @@ -787,11 +787,16 @@ public override async Task Whats_new_2021_sample_7(bool async) AssertSql( @"@__size_0='11' -SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min] +SELECT [p0].[LastName], [f].[Size], ( + SELECT MIN([f1].[Size]) + FROM [Person] AS [p1] + LEFT JOIN [Feet] AS [f0] ON [p1].[Id] = [f0].[Id] + LEFT JOIN [Person] AS [p2] ON [f0].[Id] = [p2].[Id] + LEFT JOIN [Feet] AS [f1] ON [p1].[Id] = [f1].[Id] + WHERE [f0].[Size] = @__size_0 AND [p1].[MiddleInitial] IS NOT NULL AND ([f0].[Id] <> 1 OR [f0].[Id] IS NULL) AND ([f].[Size] = [f0].[Size] OR ([f].[Size] IS NULL AND [f0].[Size] IS NULL)) AND ([p0].[LastName] = [p2].[LastName] OR ([p0].[LastName] IS NULL AND [p2].[LastName] IS NULL))) AS [Min] FROM [Person] AS [p] LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id] LEFT JOIN [Person] AS [p0] ON [f].[Id] = [p0].[Id] -LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id] WHERE [f].[Size] = @__size_0 AND [p].[MiddleInitial] IS NOT NULL AND ([f].[Id] <> 1 OR [f].[Id] IS NULL) GROUP BY [f].[Size], [p0].[LastName]"); } @@ -821,9 +826,12 @@ public override async Task Whats_new_2021_sample_9(bool async) await base.Whats_new_2021_sample_9(async); AssertSql( - @"SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total] + @"SELECT [p].[FirstName] AS [Feet], ( + SELECT COALESCE(SUM([f].[Size]), 0) + FROM [Person] AS [p0] + LEFT JOIN [Feet] AS [f] ON [p0].[Id] = [f].[Id] + WHERE [p].[FirstName] = [p0].[FirstName] OR ([p].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AS [Total] FROM [Person] AS [p] -LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id] GROUP BY [p].[FirstName]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 0ecf36737a7..b1e2f0a163b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -6250,15 +6250,15 @@ public override async Task GroupBy_with_boolean_grouping_key(bool async) await base.GroupBy_with_boolean_grouping_key(async); AssertSql( - @"SELECT [g].[CityOfBirthName], [g].[HasSoulPatch], CASE - WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [IsMarcus], COUNT(*) AS [Count] -FROM [Gears] AS [g] -GROUP BY [g].[CityOfBirthName], [g].[HasSoulPatch], CASE - WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END"); + @"SELECT [t].[CityOfBirthName], [t].[HasSoulPatch], [t].[IsMarcus], COUNT(*) AS [Count] +FROM ( + SELECT [g].[CityOfBirthName], [g].[HasSoulPatch], CASE + WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [IsMarcus] + FROM [Gears] AS [g] +) AS [t] +GROUP BY [t].[CityOfBirthName], [t].[HasSoulPatch], [t].[IsMarcus]"); } public override async Task GroupBy_with_boolean_groupin_key_thru_navigation_access(bool async) @@ -6289,8 +6289,12 @@ public override async Task Group_by_on_StartsWith_with_null_parameter_as_argumen await base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); AssertSql( - @"SELECT CAST(0 AS bit) -FROM [Gears] AS [g]"); + @"SELECT [t].[Key] +FROM ( + SELECT CAST(0 AS bit) AS [Key] + FROM [Gears] AS [g] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task Group_by_with_having_StartsWith_with_null_parameter_as_argument(bool async) @@ -6681,15 +6685,15 @@ public override async Task Group_by_nullable_property_HasValue_and_project_the_g await base.Group_by_nullable_property_HasValue_and_project_the_grouping_key(async); AssertSql( - @"SELECT CASE - WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END -FROM [Weapons] AS [w] -GROUP BY CASE - WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END"); + @"SELECT [t].[Key] +FROM ( + SELECT CASE + WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [Key] + FROM [Weapons] AS [w] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task Group_by_nullable_property_and_project_the_grouping_key_HasValue(bool async) @@ -7716,6 +7720,27 @@ FROM [Weapons] AS [w] ORDER BY [g].[Nickname], [g].[SquadId], [t].[IsAutomatic]"); } + public override async Task + Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + { + await base + .Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t0].[Key], [t0].[Count] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT [t].[Key], COUNT(*) AS [Count] + FROM ( + SELECT CAST(LEN([w].[Name]) AS int) AS [Key] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] + ) AS [t] + GROUP BY [t].[Key] +) AS [t0] +ORDER BY [g].[Nickname], [g].[SquadId]"); + } + public override async Task Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(bool async) { await base.Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(async); @@ -8207,17 +8232,6 @@ FROM [Weapons] AS [w0] ORDER BY [g].[Nickname], [g].[SquadId], [s].[Id]"); } - public override async Task - Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( - bool async) - { - await base - .Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( - async); - - AssertSql(); - } - public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column_also_projecting_complex_expressions( bool async) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 5c9bee3e9b2..c3f4165f465 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -717,10 +717,13 @@ public override async Task GroupBy_anonymous_key_type_mismatch_with_aggregate(bo await base.GroupBy_anonymous_key_type_mismatch_with_aggregate(async); AssertSql( - @"SELECT COUNT(*) AS [I0], DATEPART(year, [o].[OrderDate]) AS [I1] -FROM [Orders] AS [o] -GROUP BY DATEPART(year, [o].[OrderDate]) -ORDER BY DATEPART(year, [o].[OrderDate])"); + @"SELECT COUNT(*) AS [I0], [t].[I0] AS [I1] +FROM ( + SELECT DATEPART(year, [o].[OrderDate]) AS [I0] + FROM [Orders] AS [o] +) AS [t] +GROUP BY [t].[I0] +ORDER BY [t].[I0]"); } public override async Task GroupBy_Property_scalar_element_selector_Average(bool async) @@ -922,6 +925,19 @@ FROM [Orders] AS [o] GROUP BY [o].[OrderID]"); } + public override async Task GroupBy_conditional_properties(bool async) + { + await base.GroupBy_conditional_properties(async); + + AssertSql( + @"SELECT [t].[OrderMonth], [t].[CustomerID] AS [Customer], COUNT(*) AS [Count] +FROM ( + SELECT [o].[CustomerID], NULL AS [OrderMonth] + FROM [Orders] AS [o] +) AS [t] +GROUP BY [t].[OrderMonth], [t].[CustomerID]"); + } + public override async Task GroupBy_empty_key_Aggregate(bool async) { await base.GroupBy_empty_key_Aggregate(async); @@ -1265,6 +1281,46 @@ FROM [Orders] AS [o] GROUP BY [t].[CustomerID]"); } + public override async Task GroupBy_complex_key_aggregate(bool async) + { + await base.GroupBy_complex_key_aggregate(async); + + AssertSql( + @"SELECT [t].[Key], COUNT(*) AS [Count] +FROM ( + SELECT SUBSTRING([c].[CustomerID], 0 + 1, 1) AS [Key] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +) AS [t] +GROUP BY [t].[Key]"); + } + + public override async Task GroupBy_complex_key_aggregate_2(bool async) + { + await base.GroupBy_complex_key_aggregate_2(async); + + AssertSql( + @"SELECT [t].[Key] AS [Month], COALESCE(SUM([t].[OrderID]), 0) AS [Total], ( + SELECT COALESCE(SUM([o0].[OrderID]), 0) + FROM [Orders] AS [o0] + WHERE DATEPART(month, [o0].[OrderDate]) = [t].[Key] OR ([o0].[OrderDate] IS NULL AND [t].[Key] IS NULL)) AS [Payment] +FROM ( + SELECT [o].[OrderID], DATEPART(month, [o].[OrderDate]) AS [Key] + FROM [Orders] AS [o] +) AS [t] +GROUP BY [t].[Key]"); + } + + public override async Task Select_collection_of_scalar_before_GroupBy_aggregate(bool async) + { + await base.Select_collection_of_scalar_before_GroupBy_aggregate(async); + + AssertSql( + @"SELECT [c].[City] AS [Key], COUNT(*) AS [Count] +FROM [Customers] AS [c] +GROUP BY [c].[City]"); + } + public override async Task GroupBy_OrderBy_key(bool async) { await base.GroupBy_OrderBy_key(async); @@ -1894,9 +1950,12 @@ public override async Task GroupBy_with_aggregate_through_navigation_property(bo await base.GroupBy_with_aggregate_through_navigation_property(async); AssertSql( - @"SELECT MAX([c].[Region]) AS [max] + @"SELECT ( + SELECT MAX([c].[Region]) + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] + WHERE [o].[EmployeeID] = [o0].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o0].[EmployeeID] IS NULL)) AS [max] FROM [Orders] AS [o] -LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] GROUP BY [o].[EmployeeID]"); } @@ -1908,13 +1967,7 @@ public override async Task GroupBy_with_aggregate_containing_complex_where(bool @"SELECT [o].[EmployeeID] AS [Key], ( SELECT MAX([o0].[OrderID]) FROM [Orders] AS [o0] - WHERE CAST([o0].[EmployeeID] AS bigint) = CAST((( - SELECT MAX([o1].[OrderID]) - FROM [Orders] AS [o1] - WHERE [o].[EmployeeID] = [o1].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o1].[EmployeeID] IS NULL)) * 6) AS bigint) OR ([o0].[EmployeeID] IS NULL AND ( - SELECT MAX([o1].[OrderID]) - FROM [Orders] AS [o1] - WHERE [o].[EmployeeID] = [o1].[EmployeeID] OR ([o].[EmployeeID] IS NULL AND [o1].[EmployeeID] IS NULL)) IS NULL)) AS [Max] + WHERE CAST([o0].[EmployeeID] AS bigint) = CAST((MAX([o].[OrderID]) * 6) AS bigint) OR ([o0].[EmployeeID] IS NULL AND MAX([o].[OrderID]) IS NULL)) AS [Max] FROM [Orders] AS [o] GROUP BY [o].[EmployeeID]"); } @@ -2082,13 +2135,7 @@ 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], ( - 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] + @"SELECT [o].[CustomerID] AS [Key], MAX(DISTINCT ([o].[OrderDate])) AS [Max] FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); } @@ -2311,6 +2358,19 @@ FROM [Orders] AS [o] WHERE [o].[OrderDate] IS NOT NULL"); } + public override async Task GroupBy_nominal_type_count(bool async) + { + await base.GroupBy_nominal_type_count(async); + + AssertSql( + @"SELECT COUNT(*) +FROM ( + SELECT [o].[CustomerID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] +) AS [t]"); + } + public override async Task GroupBy_based_on_renamed_property_simple(bool async) { await base.GroupBy_based_on_renamed_property_simple(async); @@ -2602,13 +2662,16 @@ public override async Task GroupBy_aggregate_followed_another_GroupBy_aggregate( await base.GroupBy_aggregate_followed_another_GroupBy_aggregate(async); AssertSql( - @"SELECT [t].[CustomerID] AS [Key], COUNT(*) AS [Count] + @"SELECT [t0].[CustomerID] AS [Key], COUNT(*) AS [Count] FROM ( - SELECT [o].[CustomerID] - FROM [Orders] AS [o] - GROUP BY [o].[CustomerID], DATEPART(year, [o].[OrderDate]) -) AS [t] -GROUP BY [t].[CustomerID]"); + SELECT [t].[CustomerID] + FROM ( + SELECT [o].[CustomerID], DATEPART(year, [o].[OrderDate]) AS [Year] + FROM [Orders] AS [o] + ) AS [t] + GROUP BY [t].[CustomerID], [t].[Year] +) AS [t0] +GROUP BY [t0].[CustomerID]"); } public override async Task GroupBy_aggregate_without_selectMany_selecting_first(bool async) @@ -2626,20 +2689,50 @@ CROSS JOIN [Orders] AS [o0] WHERE [o0].[OrderID] = [t].[c]"); } + public override async Task GroupBy_aggregate_left_join_GroupBy_aggregate_left_join(bool async) + { + await base.GroupBy_aggregate_left_join_GroupBy_aggregate_left_join(async); + + AssertSql( + @"SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT MIN([o].[OrderID]) AS [c] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] +) AS [t] +CROSS JOIN [Orders] AS [o0] +WHERE [o0].[OrderID] = [t].[c]"); + } + + public override async Task GroupBy_selecting_grouping_key_list(bool async) + { + await base.GroupBy_selecting_grouping_key_list(async); + + AssertSql( + @"SELECT [t].[CustomerID], [o0].[CustomerID], [o0].[OrderID] +FROM ( + SELECT [o].[CustomerID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [t].[CustomerID] = [o0].[CustomerID] +ORDER BY [t].[CustomerID]"); + } + public override async Task GroupBy_with_grouping_key_using_Like(bool async) { await base.GroupBy_with_grouping_key_using_Like(async); AssertSql( - @"SELECT CASE - WHEN [o].[CustomerID] LIKE N'A%' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [Key], COUNT(*) AS [Count] -FROM [Orders] AS [o] -GROUP BY CASE - WHEN [o].[CustomerID] LIKE N'A%' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END"); + @"SELECT [t].[Key], COUNT(*) AS [Count] +FROM ( + SELECT CASE + WHEN [o].[CustomerID] LIKE N'A%' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [Key] + FROM [Orders] AS [o] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task GroupBy_with_grouping_key_DateTime_Day(bool async) @@ -2647,9 +2740,12 @@ public override async Task GroupBy_with_grouping_key_DateTime_Day(bool async) await base.GroupBy_with_grouping_key_DateTime_Day(async); AssertSql( - @"SELECT DATEPART(day, [o].[OrderDate]) AS [Key], COUNT(*) AS [Count] -FROM [Orders] AS [o] -GROUP BY DATEPART(day, [o].[OrderDate])"); + @"SELECT [t].[Key], COUNT(*) AS [Count] +FROM ( + SELECT DATEPART(day, [o].[OrderDate]) AS [Key] + FROM [Orders] AS [o] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task GroupBy_with_cast_inside_grouping_aggregate(bool async) @@ -2954,7 +3050,7 @@ public override async Task Complex_query_with_group_by_in_subquery5(bool async) AssertSql( @"SELECT [t].[c], [t].[ProductID], [t0].[CustomerID], [t0].[City] FROM ( - SELECT COALESCE(SUM([o].[ProductID] + ([o].[OrderID] * 1000)), 0) AS [c], [o].[ProductID] + SELECT COALESCE(SUM([o].[ProductID] + ([o].[OrderID] * 1000)), 0) AS [c], [o].[ProductID], MIN([o].[OrderID] / 100) AS [c0] FROM [Order Details] AS [o] INNER JOIN [Orders] AS [o0] ON [o].[OrderID] = [o0].[OrderID] LEFT JOIN [Customers] AS [c] ON [o0].[CustomerID] = [c].[CustomerID] @@ -2964,12 +3060,7 @@ GROUP BY [o].[ProductID] OUTER APPLY ( SELECT [c0].[CustomerID], [c0].[City] FROM [Customers] AS [c0] - WHERE CAST(LEN([c0].[CustomerID]) AS int) < ( - SELECT MIN([o1].[OrderID] / 100) - FROM [Order Details] AS [o1] - INNER JOIN [Orders] AS [o2] ON [o1].[OrderID] = [o2].[OrderID] - LEFT JOIN [Customers] AS [c1] ON [o2].[CustomerID] = [c1].[CustomerID] - WHERE [c1].[CustomerID] = N'ALFKI' AND [t].[ProductID] = [o1].[ProductID]) + WHERE CAST(LEN([c0].[CustomerID]) AS int) < [t].[c0] ) AS [t0] ORDER BY [t].[ProductID], [t0].[CustomerID]"); } @@ -2978,7 +3069,29 @@ public override async Task Complex_query_with_groupBy_in_subquery4(bool async) { await base.Complex_query_with_groupBy_in_subquery4(async); - AssertSql(); + AssertSql( + @"SELECT [c].[CustomerID], [t1].[Sum], [t1].[Count], [t1].[Key] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT COALESCE(SUM([t].[OrderID]), 0) AS [Sum], ( + SELECT COUNT(*) + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c1].[CustomerID] AS [CustomerID0], [c1].[Address], [c1].[City], [c1].[CompanyName], [c1].[ContactName], [c1].[ContactTitle], [c1].[Country], [c1].[Fax], [c1].[Phone], [c1].[PostalCode], [c1].[Region], COALESCE([c1].[City], N'') + COALESCE([o0].[CustomerID], N'') AS [Key] + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c1] ON [o0].[CustomerID] = [c1].[CustomerID] + WHERE [c].[CustomerID] = [o0].[CustomerID] + ) AS [t0] + LEFT JOIN [Customers] AS [c0] ON [t0].[CustomerID] = [c0].[CustomerID] + WHERE ([t].[Key] = [t0].[Key] OR ([t].[Key] IS NULL AND [t0].[Key] IS NULL)) AND (COALESCE([c0].[City], N'') + COALESCE([t0].[CustomerID], N'') LIKE N'Lon%')) AS [Count], [t].[Key] + FROM ( + SELECT [o].[OrderID], COALESCE([c2].[City], N'') + COALESCE([o].[CustomerID], N'') AS [Key] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c2] ON [o].[CustomerID] = [c2].[CustomerID] + WHERE [c].[CustomerID] = [o].[CustomerID] + ) AS [t] + GROUP BY [t].[Key] +) AS [t1] +ORDER BY [c].[CustomerID]"); } public override async Task GroupBy_aggregate_SelectMany(bool async) @@ -3044,6 +3157,36 @@ public override async Task GroupBy_Distinct(bool async) AssertSql(); } + public override async Task GroupBy_complex_key_without_aggregate(bool async) + { + await base.GroupBy_complex_key_without_aggregate(async); + + AssertSql( + @"SELECT [t0].[Key], [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate], [t1].[CustomerID0] +FROM ( + SELECT [t].[Key] + FROM ( + SELECT SUBSTRING([c].[CustomerID], 0 + 1, 1) AS [Key] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] + ) AS [t] + GROUP BY [t].[Key] +) AS [t0] +LEFT JOIN ( + SELECT [t2].[OrderID], [t2].[CustomerID], [t2].[EmployeeID], [t2].[OrderDate], [t2].[CustomerID0], [t2].[Key] + FROM ( + SELECT [t3].[OrderID], [t3].[CustomerID], [t3].[EmployeeID], [t3].[OrderDate], [t3].[CustomerID0], [t3].[Key], ROW_NUMBER() OVER(PARTITION BY [t3].[Key] ORDER BY [t3].[OrderID], [t3].[CustomerID0]) AS [row] + FROM ( + SELECT [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate], [c0].[CustomerID] AS [CustomerID0], SUBSTRING([c0].[CustomerID], 0 + 1, 1) AS [Key] + FROM [Orders] AS [o0] + LEFT JOIN [Customers] AS [c0] ON [o0].[CustomerID] = [c0].[CustomerID] + ) AS [t3] + ) AS [t2] + WHERE 1 < [t2].[row] AND [t2].[row] <= 3 +) AS [t1] ON [t0].[Key] = [t1].[Key] +ORDER BY [t0].[Key], [t1].[OrderID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index 56a4e4ff85e..a86d80fbc51 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -1872,18 +1872,21 @@ public override async Task Correlated_collection_after_groupby_with_complex_proj await base.Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(async); AssertSql( - @"SELECT [t].[OrderID], [t].[c], [t0].[Outer], [t0].[Inner], [t0].[OrderDate] + @"SELECT [t0].[OrderID], [t0].[Complex], [t1].[Outer], [t1].[Inner], [t1].[OrderDate] FROM ( - SELECT [o].[OrderID], DATEPART(month, [o].[OrderDate]) AS [c] - FROM [Orders] AS [o] - GROUP BY [o].[OrderID], DATEPART(month, [o].[OrderDate]) -) AS [t] + SELECT [t].[OrderID], [t].[Complex] + FROM ( + SELECT [o].[OrderID], DATEPART(month, [o].[OrderDate]) AS [Complex] + FROM [Orders] AS [o] + ) AS [t] + GROUP BY [t].[OrderID], [t].[Complex] +) AS [t0] OUTER APPLY ( - SELECT [t].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] + SELECT [t0].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] FROM [Orders] AS [o0] - WHERE [o0].[OrderID] = [t].[OrderID] AND [o0].[OrderID] IN (10248, 10249, 10250) -) AS [t0] -ORDER BY [t].[OrderID]"); + WHERE [o0].[OrderID] = [t0].[OrderID] AND [o0].[OrderID] IN (10248, 10249, 10250) +) AS [t1] +ORDER BY [t0].[OrderID]"); } public override async Task Select_nested_collection_deep(bool async) @@ -2249,7 +2252,22 @@ public override async Task Correlated_collection_after_groupby_with_complex_proj { await base.Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier(async); - AssertSql(); + AssertSql( + @"SELECT [t0].[CustomerID], [t0].[Complex], [t1].[Outer], [t1].[Inner], [t1].[OrderDate] +FROM ( + SELECT [t].[CustomerID], [t].[Complex] + FROM ( + SELECT [o].[CustomerID], DATEPART(month, [o].[OrderDate]) AS [Complex] + FROM [Orders] AS [o] + ) AS [t] + GROUP BY [t].[CustomerID], [t].[Complex] +) AS [t0] +OUTER APPLY ( + SELECT [t0].[CustomerID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE ([o0].[CustomerID] = [t0].[CustomerID] OR ([o0].[CustomerID] IS NULL AND [t0].[CustomerID] IS NULL)) AND [o0].[OrderID] IN (10248, 10249, 10250) +) AS [t1] +ORDER BY [t0].[CustomerID], [t0].[Complex]"); } public override async Task Select_bool_closure_with_order_by_property_with_cast_to_nullable(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index f267831a442..c18f0bc59d4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -1071,13 +1071,35 @@ 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([s].[Id] AS float)) AS [p1], COALESCE(SUM([s].[Id]), 0) AS [p2], MAX(CAST(LEN([s].[Name]) AS int)) AS [p3] + @"SELECT ( + SELECT AVG(CAST([s].[Id] AS float)) + FROM ( + SELECT [o0].[Id], [o0].[Discriminator], [o0].[Name], 1 AS [Key], [o0].[PersonAddress_AddressLine], [o0].[PersonAddress_PlaceType], [o0].[PersonAddress_ZipCode], [o0].[PersonAddress_Country_Name], [o0].[PersonAddress_Country_PlanetId] + FROM [OwnedPerson] AS [o0] + ) AS [t0] + LEFT JOIN [Planet] AS [p] ON [t0].[PersonAddress_Country_PlanetId] = [p].[Id] + LEFT JOIN [Star] AS [s] ON [p].[StarId] = [s].[Id] + WHERE [t].[Key] = [t0].[Key]) AS [p1], ( + SELECT COALESCE(SUM([s0].[Id]), 0) + FROM ( + SELECT [o1].[Id], [o1].[Discriminator], [o1].[Name], 1 AS [Key], [o1].[PersonAddress_AddressLine], [o1].[PersonAddress_PlaceType], [o1].[PersonAddress_ZipCode], [o1].[PersonAddress_Country_Name], [o1].[PersonAddress_Country_PlanetId] + FROM [OwnedPerson] AS [o1] + ) AS [t1] + LEFT JOIN [Planet] AS [p0] ON [t1].[PersonAddress_Country_PlanetId] = [p0].[Id] + LEFT JOIN [Star] AS [s0] ON [p0].[StarId] = [s0].[Id] + WHERE [t].[Key] = [t1].[Key]) AS [p2], ( + SELECT MAX(CAST(LEN([s1].[Name]) AS int)) + FROM ( + SELECT [o2].[Id], [o2].[Discriminator], [o2].[Name], 1 AS [Key], [o2].[PersonAddress_AddressLine], [o2].[PersonAddress_PlaceType], [o2].[PersonAddress_ZipCode], [o2].[PersonAddress_Country_Name], [o2].[PersonAddress_Country_PlanetId] + FROM [OwnedPerson] AS [o2] + ) AS [t2] + LEFT JOIN [Planet] AS [p1] ON [t2].[PersonAddress_Country_PlanetId] = [p1].[Id] + LEFT JOIN [Star] AS [s1] ON [p1].[StarId] = [s1].[Id] + WHERE [t].[Key] = [t2].[Key]) AS [p3] FROM ( - SELECT 1 AS [Key], [o].[PersonAddress_Country_PlanetId] + SELECT 1 AS [Key] FROM [OwnedPerson] AS [o] ) 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/SimpleQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs index 03754ad1c0a..b0255e10d1b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs @@ -217,13 +217,22 @@ public override async Task GroupBy_Aggregate_over_navigations_repeated(bool asyn await base.GroupBy_Aggregate_over_navigations_repeated(async); AssertSql( - @"SELECT MIN([o].[HourlyRate]) AS [HourlyRate], MIN([c].[Id]) AS [CustomerId], MIN([c0].[Name]) AS [CustomerName] + @"SELECT ( + SELECT MIN([o].[HourlyRate]) + FROM [TimeSheets] AS [t0] + LEFT JOIN [Order] AS [o] ON [t0].[OrderId] = [o].[Id] + WHERE [t0].[OrderId] IS NOT NULL AND [t].[OrderId] = [t0].[OrderId]) AS [HourlyRate], ( + SELECT MIN([c].[Id]) + FROM [TimeSheets] AS [t1] + INNER JOIN [Project] AS [p] ON [t1].[ProjectId] = [p].[Id] + INNER JOIN [Customers] AS [c] ON [p].[CustomerId] = [c].[Id] + WHERE [t1].[OrderId] IS NOT NULL AND [t].[OrderId] = [t1].[OrderId]) AS [CustomerId], ( + SELECT MIN([c0].[Name]) + FROM [TimeSheets] AS [t2] + INNER JOIN [Project] AS [p0] ON [t2].[ProjectId] = [p0].[Id] + INNER JOIN [Customers] AS [c0] ON [p0].[CustomerId] = [c0].[Id] + WHERE [t2].[OrderId] IS NOT NULL AND [t].[OrderId] = [t2].[OrderId]) AS [CustomerName] FROM [TimeSheets] AS [t] -LEFT JOIN [Order] AS [o] ON [t].[OrderId] = [o].[Id] -INNER JOIN [Project] AS [p] ON [t].[ProjectId] = [p].[Id] -INNER JOIN [Customers] AS [c] ON [p].[CustomerId] = [c].[Id] -INNER JOIN [Project] AS [p0] ON [t].[ProjectId] = [p0].[Id] -INNER JOIN [Customers] AS [c0] ON [p0].[CustomerId] = [c0].[Id] WHERE [t].[OrderId] IS NOT NULL GROUP BY [t].[OrderId]"); } @@ -250,13 +259,7 @@ public override async Task Aggregate_over_subquery_in_group_by_projection_2(bool @"SELECT [t].[Value] AS [A], ( SELECT MAX([t0].[Id]) FROM [Table] AS [t0] - WHERE [t0].[Value] = (( - SELECT MAX([t1].[Id]) - FROM [Table] AS [t1] - WHERE [t].[Value] = [t1].[Value] OR ([t].[Value] IS NULL AND [t1].[Value] IS NULL)) * 6) OR ([t0].[Value] IS NULL AND ( - SELECT MAX([t1].[Id]) - FROM [Table] AS [t1] - WHERE [t].[Value] = [t1].[Value] OR ([t].[Value] IS NULL AND [t1].[Value] IS NULL)) IS NULL)) AS [B] + WHERE [t0].[Value] = (MAX([t].[Id]) * 6) OR ([t0].[Value] IS NULL AND MAX([t].[Id]) IS NULL)) AS [B] FROM [Table] AS [t] GROUP BY [t].[Value]"); } @@ -267,10 +270,7 @@ public override async Task Group_by_aggregate_in_subquery_projection_after_group AssertSql( @"SELECT [t].[Value] AS [A], COALESCE(SUM([t].[Id]), 0) AS [B], COALESCE(( - SELECT TOP(1) ( - SELECT COALESCE(SUM([t1].[Id]), 0) - FROM [Table] AS [t1] - WHERE [t].[Value] = [t1].[Value] OR ([t].[Value] IS NULL AND [t1].[Value] IS NULL)) + COALESCE(SUM([t0].[Id]), 0) + SELECT TOP(1) COALESCE(SUM([t].[Id]), 0) + COALESCE(SUM([t0].[Id]), 0) FROM [Table] AS [t0] GROUP BY [t0].[Value] ORDER BY (SELECT 1)), 0) AS [C] @@ -283,13 +283,31 @@ public override async Task Group_by_multiple_aggregate_joining_different_tables( await base.Group_by_multiple_aggregate_joining_different_tables(async); AssertSql( - @"SELECT COUNT(DISTINCT ([c].[Value1])) AS [Test1], COUNT(DISTINCT ([c0].[Value2])) AS [Test2] + @"SELECT ( + SELECT COUNT(*) + FROM ( + SELECT DISTINCT [c].[Value1] + FROM ( + SELECT [p0].[Id], [p0].[Child1Id], [p0].[Child2Id], [p0].[ChildFilter1Id], [p0].[ChildFilter2Id], 1 AS [Key] + FROM [Parents] AS [p0] + ) AS [t1] + LEFT JOIN [Child1] AS [c] ON [t1].[Child1Id] = [c].[Id] + WHERE [t].[Key] = [t1].[Key] + ) AS [t0]) AS [Test1], ( + SELECT COUNT(*) + FROM ( + SELECT DISTINCT [c0].[Value2] + FROM ( + SELECT [p1].[Id], [p1].[Child1Id], [p1].[Child2Id], [p1].[ChildFilter1Id], [p1].[ChildFilter2Id], 1 AS [Key] + FROM [Parents] AS [p1] + ) AS [t3] + LEFT JOIN [Child2] AS [c0] ON [t3].[Child2Id] = [c0].[Id] + WHERE [t].[Key] = [t3].[Key] + ) AS [t2]) AS [Test2] FROM ( - SELECT [p].[Child1Id], [p].[Child2Id], 1 AS [Key] + SELECT 1 AS [Key] FROM [Parents] AS [p] ) AS [t] -LEFT JOIN [Child1] AS [c] ON [t].[Child1Id] = [c].[Id] -LEFT JOIN [Child2] AS [c0] ON [t].[Child2Id] = [c0].[Id] GROUP BY [t].[Key]"); } @@ -298,27 +316,39 @@ public override async Task Group_by_multiple_aggregate_joining_different_tables_ await base.Group_by_multiple_aggregate_joining_different_tables_with_query_filter(async); AssertSql( - @"SELECT COUNT(DISTINCT ([t0].[Value1])) AS [Test1], ( - SELECT DISTINCT COUNT(DISTINCT ([t2].[Value2])) + @"SELECT ( + SELECT COUNT(*) + FROM ( + SELECT DISTINCT [t2].[Value1] + FROM ( + SELECT [p0].[Id], [p0].[Child1Id], [p0].[Child2Id], [p0].[ChildFilter1Id], [p0].[ChildFilter2Id], 1 AS [Key] + FROM [Parents] AS [p0] + ) AS [t0] + LEFT JOIN ( + SELECT [c].[Id], [c].[Filter1], [c].[Value1] + FROM [ChildFilter1] AS [c] + WHERE [c].[Filter1] = N'Filter1' + ) AS [t2] ON [t0].[ChildFilter1Id] = [t2].[Id] + WHERE [t].[Key] = [t0].[Key] + ) AS [t1]) AS [Test1], ( + SELECT COUNT(*) FROM ( - SELECT [p0].[Id], [p0].[Child1Id], [p0].[Child2Id], [p0].[ChildFilter1Id], [p0].[ChildFilter2Id], 1 AS [Key] - FROM [Parents] AS [p0] - ) AS [t1] - LEFT JOIN ( - SELECT [c0].[Id], [c0].[Filter2], [c0].[Value2] - FROM [ChildFilter2] AS [c0] - WHERE [c0].[Filter2] = N'Filter2' - ) AS [t2] ON [t1].[ChildFilter2Id] = [t2].[Id] - WHERE [t].[Key] = [t1].[Key]) AS [Test2] + SELECT DISTINCT [t5].[Value2] + FROM ( + SELECT [p1].[Id], [p1].[Child1Id], [p1].[Child2Id], [p1].[ChildFilter1Id], [p1].[ChildFilter2Id], 1 AS [Key] + FROM [Parents] AS [p1] + ) AS [t4] + LEFT JOIN ( + SELECT [c0].[Id], [c0].[Filter2], [c0].[Value2] + FROM [ChildFilter2] AS [c0] + WHERE [c0].[Filter2] = N'Filter2' + ) AS [t5] ON [t4].[ChildFilter2Id] = [t5].[Id] + WHERE [t].[Key] = [t4].[Key] + ) AS [t3]) AS [Test2] FROM ( - SELECT [p].[ChildFilter1Id], 1 AS [Key] + SELECT 1 AS [Key] FROM [Parents] AS [p] ) AS [t] -LEFT JOIN ( - SELECT [c].[Id], [c].[Value1] - FROM [ChildFilter1] AS [c] - WHERE [c].[Filter1] = N'Filter1' -) AS [t0] ON [t].[ChildFilter1Id] = [t0].[Id] GROUP BY [t].[Key]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 63f92e41842..da2da65afc9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -7393,15 +7393,15 @@ public override async Task GroupBy_with_boolean_grouping_key(bool async) await base.GroupBy_with_boolean_grouping_key(async); AssertSql( - @"SELECT [g].[CityOfBirthName], [g].[HasSoulPatch], CASE - WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [IsMarcus], COUNT(*) AS [Count] -FROM [Gears] AS [g] -GROUP BY [g].[CityOfBirthName], [g].[HasSoulPatch], CASE - WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END"); + @"SELECT [t].[CityOfBirthName], [t].[HasSoulPatch], [t].[IsMarcus], COUNT(*) AS [Count] +FROM ( + SELECT [g].[CityOfBirthName], [g].[HasSoulPatch], CASE + WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [IsMarcus] + FROM [Gears] AS [g] +) AS [t] +GROUP BY [t].[CityOfBirthName], [t].[HasSoulPatch], [t].[IsMarcus]"); } public override async Task GroupBy_with_boolean_groupin_key_thru_navigation_access(bool async) @@ -7435,8 +7435,12 @@ public override async Task Group_by_on_StartsWith_with_null_parameter_as_argumen await base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); AssertSql( - @"SELECT CAST(0 AS bit) -FROM [Gears] AS [g]"); + @"SELECT [t].[Key] +FROM ( + SELECT CAST(0 AS bit) AS [Key] + FROM [Gears] AS [g] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task Group_by_with_having_StartsWith_with_null_parameter_as_argument(bool async) @@ -7874,15 +7878,15 @@ public override async Task Group_by_nullable_property_HasValue_and_project_the_g await base.Group_by_nullable_property_HasValue_and_project_the_grouping_key(async); AssertSql( - @"SELECT CASE - WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END -FROM [Weapons] AS [w] -GROUP BY CASE - WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END"); + @"SELECT [t].[Key] +FROM ( + SELECT CASE + WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [Key] + FROM [Weapons] AS [w] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task Group_by_nullable_property_and_project_the_grouping_key_HasValue(bool async) @@ -9300,6 +9304,27 @@ FROM [Weapons] AS [w] ORDER BY [g].[Nickname], [g].[SquadId], [t].[IsAutomatic]"); } + public override async Task + Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + { + await base + .Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t0].[Key], [t0].[Count] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT [t].[Key], COUNT(*) AS [Count] + FROM ( + SELECT CAST(LEN([w].[Name]) AS int) AS [Key] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] + ) AS [t] + GROUP BY [t].[Key] +) AS [t0] +ORDER BY [g].[Nickname], [g].[SquadId]"); + } + public override async Task Sum_with_no_data_nullable_double(bool async) { await base.Sum_with_no_data_nullable_double(async); @@ -9593,17 +9618,6 @@ public override async Task Project_discriminator_columns(bool async) AssertSql(); } - public override async Task - Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( - bool async) - { - await base - .Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( - async); - - AssertSql(); - } - public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column_also_projecting_complex_expressions( bool async) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index 35c7530d7fe..069b992c86b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -2070,15 +2070,15 @@ public override async Task GroupBy_with_boolean_grouping_key(bool async) await base.GroupBy_with_boolean_grouping_key(async); AssertSql( - @"SELECT [g].[CityOfBirthName], [g].[HasSoulPatch], CASE - WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END AS [IsMarcus], COUNT(*) AS [Count] -FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] -GROUP BY [g].[CityOfBirthName], [g].[HasSoulPatch], CASE - WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END"); + @"SELECT [t].[CityOfBirthName], [t].[HasSoulPatch], [t].[IsMarcus], COUNT(*) AS [Count] +FROM ( + SELECT [g].[CityOfBirthName], [g].[HasSoulPatch], CASE + WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [IsMarcus] + FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +) AS [t] +GROUP BY [t].[CityOfBirthName], [t].[HasSoulPatch], [t].[IsMarcus]"); } public override async Task Correlated_collections_with_Distinct(bool async) @@ -3804,8 +3804,12 @@ public override async Task Group_by_on_StartsWith_with_null_parameter_as_argumen await base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); AssertSql( - @"SELECT CAST(0 AS bit) -FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g]"); + @"SELECT [t].[Key] +FROM ( + SELECT CAST(0 AS bit) AS [Key] + FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task Where_is_properly_lifted_from_subquery_created_by_include(bool async) @@ -5853,15 +5857,15 @@ public override async Task Group_by_nullable_property_HasValue_and_project_the_g await base.Group_by_nullable_property_HasValue_and_project_the_grouping_key(async); AssertSql( - @"SELECT CASE - WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END -FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] -GROUP BY CASE - WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END"); + @"SELECT [t].[Key] +FROM ( + SELECT CASE + WHEN [w].[SynergyWithId] IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END AS [Key] + FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] +) AS [t] +GROUP BY [t].[Key]"); } public override async Task Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result3(bool async) @@ -8160,7 +8164,19 @@ await base .Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( async); - AssertSql(); + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t0].[Key], [t0].[Count] +FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +OUTER APPLY ( + SELECT [t].[Key], COUNT(*) AS [Count] + FROM ( + SELECT CAST(LEN([w].[Name]) AS int) AS [Key] + FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] + ) AS [t] + GROUP BY [t].[Key] +) AS [t0] +ORDER BY [g].[Nickname], [g].[SquadId]"); } public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column_also_projecting_complex_expressions( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs index c71cc77a64b..7d5687fdec1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs @@ -1066,13 +1066,35 @@ 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([s].[Id] AS float)) AS [p1], COALESCE(SUM([s].[Id]), 0) AS [p2], MAX(CAST(LEN([s].[Name]) AS int)) AS [p3] + @"SELECT ( + SELECT AVG(CAST([s].[Id] AS float)) + FROM ( + SELECT [o0].[Id], [o0].[Discriminator], [o0].[Name], [o0].[PeriodEnd], [o0].[PeriodStart], 1 AS [Key], [o0].[PersonAddress_AddressLine], [o0].[PeriodEnd] AS [PeriodEnd0], [o0].[PeriodStart] AS [PeriodStart0], [o0].[PersonAddress_PlaceType], [o0].[PersonAddress_ZipCode], [o0].[PersonAddress_Country_Name], [o0].[PersonAddress_Country_PlanetId] + FROM [OwnedPerson] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [o0] + ) AS [t0] + LEFT JOIN [Planet] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [p] ON [t0].[PersonAddress_Country_PlanetId] = [p].[Id] + LEFT JOIN [Star] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] ON [p].[StarId] = [s].[Id] + WHERE [t].[Key] = [t0].[Key]) AS [p1], ( + SELECT COALESCE(SUM([s0].[Id]), 0) + FROM ( + SELECT [o1].[Id], [o1].[Discriminator], [o1].[Name], [o1].[PeriodEnd], [o1].[PeriodStart], 1 AS [Key], [o1].[PersonAddress_AddressLine], [o1].[PeriodEnd] AS [PeriodEnd0], [o1].[PeriodStart] AS [PeriodStart0], [o1].[PersonAddress_PlaceType], [o1].[PersonAddress_ZipCode], [o1].[PersonAddress_Country_Name], [o1].[PersonAddress_Country_PlanetId] + FROM [OwnedPerson] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [o1] + ) AS [t1] + LEFT JOIN [Planet] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [p0] ON [t1].[PersonAddress_Country_PlanetId] = [p0].[Id] + LEFT JOIN [Star] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s0] ON [p0].[StarId] = [s0].[Id] + WHERE [t].[Key] = [t1].[Key]) AS [p2], ( + SELECT MAX(CAST(LEN([s1].[Name]) AS int)) + FROM ( + SELECT [o2].[Id], [o2].[Discriminator], [o2].[Name], [o2].[PeriodEnd], [o2].[PeriodStart], 1 AS [Key], [o2].[PersonAddress_AddressLine], [o2].[PeriodEnd] AS [PeriodEnd0], [o2].[PeriodStart] AS [PeriodStart0], [o2].[PersonAddress_PlaceType], [o2].[PersonAddress_ZipCode], [o2].[PersonAddress_Country_Name], [o2].[PersonAddress_Country_PlanetId] + FROM [OwnedPerson] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [o2] + ) AS [t2] + LEFT JOIN [Planet] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [p1] ON [t2].[PersonAddress_Country_PlanetId] = [p1].[Id] + LEFT JOIN [Star] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s1] ON [p1].[StarId] = [s1].[Id] + WHERE [t].[Key] = [t2].[Key]) AS [p3] FROM ( - SELECT 1 AS [Key], [o].[PersonAddress_Country_PlanetId] + SELECT 1 AS [Key] FROM [OwnedPerson] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [o] ) AS [t] -LEFT JOIN [Planet] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [p] ON [t].[PersonAddress_Country_PlanetId] = [p].[Id] -LEFT JOIN [Star] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] ON [p].[StarId] = [s].[Id] GROUP BY [t].[Key]"); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index b6cd2693f52..f7b6840cab4 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -2075,8 +2075,12 @@ public override async Task Group_by_on_StartsWith_with_null_parameter_as_argumen await base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); AssertSql( - @"SELECT 0 -FROM ""Gears"" AS ""g"""); + @"SELECT ""t"".""Key"" +FROM ( + SELECT 0 AS ""Key"" + FROM ""Gears"" AS ""g"" +) AS ""t"" +GROUP BY ""t"".""Key"""); } public override async Task Non_unicode_parameter_is_used_for_non_unicode_column(bool async) @@ -2134,9 +2138,12 @@ public override async Task GroupBy_with_boolean_grouping_key(bool async) await base.GroupBy_with_boolean_grouping_key(async); AssertSql( - @"SELECT ""g"".""CityOfBirthName"", ""g"".""HasSoulPatch"", ""g"".""Nickname"" = 'Marcus' AS ""IsMarcus"", COUNT(*) AS ""Count"" -FROM ""Gears"" AS ""g"" -GROUP BY ""g"".""CityOfBirthName"", ""g"".""HasSoulPatch"", ""g"".""Nickname"" = 'Marcus'"); + @"SELECT ""t"".""CityOfBirthName"", ""t"".""HasSoulPatch"", ""t"".""IsMarcus"", COUNT(*) AS ""Count"" +FROM ( + SELECT ""g"".""CityOfBirthName"", ""g"".""HasSoulPatch"", ""g"".""Nickname"" = 'Marcus' AS ""IsMarcus"" + FROM ""Gears"" AS ""g"" +) AS ""t"" +GROUP BY ""t"".""CityOfBirthName"", ""t"".""HasSoulPatch"", ""t"".""IsMarcus"""); } public override async Task Correlated_collections_on_select_many(bool async) @@ -6866,9 +6873,12 @@ public override async Task Group_by_nullable_property_HasValue_and_project_the_g await base.Group_by_nullable_property_HasValue_and_project_the_grouping_key(async); AssertSql( - @"SELECT ""w"".""SynergyWithId"" IS NOT NULL -FROM ""Weapons"" AS ""w"" -GROUP BY ""w"".""SynergyWithId"" IS NOT NULL"); + @"SELECT ""t"".""Key"" +FROM ( + SELECT ""w"".""SynergyWithId"" IS NOT NULL AS ""Key"" + FROM ""Weapons"" AS ""w"" +) AS ""t"" +GROUP BY ""t"".""Key"""); } public override async Task Query_with_complex_let_containing_ordering_and_filter_projecting_firstOrDefault_element_of_let(bool async) @@ -7679,9 +7689,10 @@ public override async Task Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( bool async) { - await base - .Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection( - async); + Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async))).Message); AssertSql(); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs index b8a56bfd12b..73294bad798 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs @@ -37,6 +37,9 @@ public override Task Complex_query_with_groupBy_in_subquery2(bool async) public override Task Complex_query_with_groupBy_in_subquery3(bool async) => AssertApplyNotSupported(() => base.Complex_query_with_groupBy_in_subquery3(async)); + public override Task Complex_query_with_groupBy_in_subquery4(bool async) + => AssertApplyNotSupported(() => base.Complex_query_with_groupBy_in_subquery4(async)); + public override Task Select_nested_collection_with_groupby(bool async) => AssertApplyNotSupported(() => base.Select_nested_collection_with_groupby(async)); @@ -46,6 +49,9 @@ public override Task Complex_query_with_group_by_in_subquery5(bool async) public override Task GroupBy_aggregate_from_multiple_query_in_same_projection(bool async) => AssertApplyNotSupported(() => base.GroupBy_aggregate_from_multiple_query_in_same_projection(async)); + public override Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(bool async) + => AssertApplyNotSupported(() => base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(async)); + public override Task GroupBy_aggregate_from_multiple_query_in_same_projection_3(bool async) => Assert.ThrowsAsync( () => base.GroupBy_aggregate_from_multiple_query_in_same_projection_3(async)); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs index ec1317689f9..d3879bff599 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs @@ -282,6 +282,12 @@ public override async Task Take_on_correlated_collection_in_first(bool async) (await Assert.ThrowsAsync( () => base.Take_on_correlated_collection_in_first(async))).Message); + public override async Task Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier(async))).Message); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs index 715730693c7..1c0b05766e1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs @@ -194,6 +194,12 @@ public override async Task Correlated_collections_with_Distinct(bool async) (await Assert.ThrowsAsync( () => base.Correlated_collections_with_Distinct(async))).Message); + public override async Task Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async))).Message); + public override async Task Negate_on_binary_expression(bool async) { await base.Negate_on_binary_expression(async);