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);