From 390b5b0e64ab62a1eba60d9ad41a3dfe1b9fc022 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 30 Jul 2024 21:17:29 +0200 Subject: [PATCH] Transform aggregate functions over subqueries on SQL Server (#34262) Closes #34256 --- .../SqlExpressions/ProjectionExpression.cs | 1 - .../Query/SqlExpressions/SelectExpression.cs | 6 +- ...erverAggregateOverSubqueryPostprocessor.cs | 202 ++++++++ .../SqlServerQueryCompilationContext.cs | 12 - .../SqlServerQueryTranslationPostprocessor.cs | 7 +- ...yableMethodTranslatingExpressionVisitor.cs | 141 ------ ...qlServerSqlTranslatingExpressionVisitor.cs | 22 - ...thwindAggregateOperatorsQueryCosmosTest.cs | 74 ++- ...windAggregateOperatorsQueryInMemoryTest.cs | 6 + ...orthwindAggregateOperatorsQueryTestBase.cs | 57 ++- ...indAggregateOperatorsQuerySqlServerTest.cs | 459 +++++++++++------- ...thwindAggregateOperatorsQuerySqliteTest.cs | 9 +- 12 files changed, 608 insertions(+), 388 deletions(-) create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerAggregateOverSubqueryPostprocessor.cs diff --git a/src/EFCore.Relational/Query/SqlExpressions/ProjectionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ProjectionExpression.cs index 28c1c6bc33e..89d0a670bb8 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ProjectionExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ProjectionExpression.cs @@ -23,7 +23,6 @@ public sealed class ProjectionExpression : Expression, IRelationalQuotableExpres /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - [EntityFrameworkInternal] public ProjectionExpression(SqlExpression expression, string alias) { Expression = expression; diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index a6c82c0b40c..a8e2291ac9f 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -111,10 +111,10 @@ public SelectExpression( IReadOnlyList orderings, SqlExpression? offset, SqlExpression? limit, - IReadOnlySet tags, - IReadOnlyDictionary? annotations) + IReadOnlySet? tags = null, + IReadOnlyDictionary? annotations = null) : this(alias, tables.ToList(), predicate, groupBy.ToList(), having, projections.ToList(), distinct, orderings.ToList(), - offset, limit, tags.ToHashSet(), annotations, sqlAliasManager: null, isMutable: false) + offset, limit, tags?.ToHashSet() ?? [], annotations, sqlAliasManager: null, isMutable: false) { } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateOverSubqueryPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateOverSubqueryPostprocessor.cs new file mode 100644 index 00000000000..9f9fc6288d4 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateOverSubqueryPostprocessor.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// SQL Server doesn't support aggregate function invocations over subqueries, or other aggregate function invocations; this +/// postprocessor lifts such subqueries out to an OUTER APPLY/JOIN on the SELECT to work around this limitation. +/// +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerAggregateOverSubqueryPostprocessor(SqlAliasManager sqlAliasManager) : ExpressionVisitor +{ + private SelectExpression? _currentSelect; + private bool _inAggregateInvocation; + private bool _aggregateArgumentContainsSubquery; + private List? _joinsToAdd; + private bool _isCorrelatedSubquery; + private HashSet? _tableAliasesInScope; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitExtension(Expression node) + { + switch (node) + { + case SelectExpression select: + { + var (parentSelect, parentJoinsToAdd, parentAggregateInvocation) = (_currentSelect, _joinsToAdd, _inAggregateInvocation); + (_currentSelect, _joinsToAdd, _inAggregateInvocation) = (select, null, false); + + // If _tableAliasesInScope is non-null, we're tracking which table aliases are in scope for the current subquery, to detect + // correlated vs. uncorrelated subqueries. Add and remove the select's tables to _tableAliasInScope. + SelectExpression visitedSelect; + if (_tableAliasesInScope is null) + { + visitedSelect = (SelectExpression)base.VisitExtension(node); + } + else + { + List tableAliases = select.Tables.Select(t => t.UnwrapJoin().Alias).Where(a => a is not null).ToList()!; + _tableAliasesInScope.UnionWith(tableAliases); + visitedSelect = (SelectExpression)base.VisitExtension(node); + _tableAliasesInScope.ExceptWith(tableAliases); + } + + // A subquery is being lifted out somewhere inside this SelectExpression; add the join. + if (_joinsToAdd is not null) + { + visitedSelect = visitedSelect.Update( + [.. visitedSelect.Tables, .. _joinsToAdd], + visitedSelect.Predicate, + visitedSelect.GroupBy, + visitedSelect.Having, + visitedSelect.Projection, + visitedSelect.Orderings, + visitedSelect.Offset, + visitedSelect.Limit); + } + + (_currentSelect, _joinsToAdd, _inAggregateInvocation) = (parentSelect, parentJoinsToAdd, parentAggregateInvocation); + return visitedSelect; + } + + // TODO: We currently don't represent the fact that a function is an aggregate or not; so for now we just match a few well-known + // functions. Improve this in the future. + case SqlFunctionExpression { IsBuiltIn: true } function + when function.Name.ToLower(CultureInfo.InvariantCulture) is "sum" or "avg" or "min" or "max" or "count": + { + var parentInAggregateInvocation = _inAggregateInvocation; + var parentIsCorrelatedSubquery = _isCorrelatedSubquery; + var parentTableAliasesInScope = _tableAliasesInScope; + var parentAggregateArgumentContainsSubquery = _aggregateArgumentContainsSubquery; + _inAggregateInvocation = true; + _isCorrelatedSubquery = false; + _tableAliasesInScope = new(); + _aggregateArgumentContainsSubquery = false; + + var result = base.VisitExtension(function); + + if (_aggregateArgumentContainsSubquery) + { + // During our visitation of the aggregate function invocation, a subquery was encountered - this is our trigger to + // extract out the argument to be an OUTER APPLY/CROSS JOIN. + if (result is not SqlFunctionExpression { Instance: null, Arguments: [var argument] } visitedFunction) + { + throw new UnreachableException(); + } + + // Since the subquery is currently a scalar subquery (or EXISTS), its doesn't have an alias for the subquery, and may + // not have an alias on its projection either. As part of lifting it out, we need to assign both aliases, so that the + // projection can be referenced. + var subqueryAlias = sqlAliasManager.GenerateTableAlias("subquery"); + + SelectExpression liftedSubquery; + + if (argument is ScalarSubqueryExpression { Subquery: { Projection: [var subqueryProjection] } subquery }) + { + // In the regular, simple case (see else below), we simply extract the entire argument of the aggregate method, + // wrap it in a simple subquery, and add that to the containing SelectExpression. + // But if the aggregate argument happens to be a scalar subqueries directly, wrapping it in a subquery isn't needed: + // we can simply use that scalar subquery directly. + + // Note that there's an assumption here that the scalar subquery being extracted out will only ever return a single + // row (and column); if it didn't, the APPLY/JOIN would cause the principal row to get duplicated, producing + // incorrect results. It shouldn't be possible to produce such a state of affairs with LINQ, and in any case, + // placing a multiple row/column-returning subquery inside ScalarSubqueryExpression is a bug - that SQL would fail + // in any case even if it weren't wrapped inside an aggregate function invocation. + if (subqueryProjection.Alias is null or "") + { + subqueryProjection = new ProjectionExpression(subqueryProjection.Expression, "value"); + } + + liftedSubquery = subquery + .Update( + subquery.Tables, + subquery.Predicate, + subquery.GroupBy, + subquery.Having, + [subqueryProjection], + subquery.Orderings, + subquery.Offset, + subquery.Limit) + .WithAlias(subqueryAlias); + } + else + { +#pragma warning disable EF1001 // SelectExpression constructor is internal + liftedSubquery = new SelectExpression( + subqueryAlias, + tables: Array.Empty(), + predicate: null, + groupBy: Array.Empty(), + having: null, + projections: new[] { new ProjectionExpression(argument, "value") }, + distinct: false, + orderings: Array.Empty(), + offset: null, + limit: null); +#pragma warning restore EF1001 + } + + _joinsToAdd ??= new(); + _joinsToAdd.Add( + _isCorrelatedSubquery ? new OuterApplyExpression(liftedSubquery) : new CrossJoinExpression(liftedSubquery)); + + var projection = liftedSubquery.Projection.Single(); + + return visitedFunction.Update( + instance: null, + arguments: + [ + new ColumnExpression( + projection.Alias, subqueryAlias, projection.Expression.Type, projection.Expression.TypeMapping, + nullable: true) + ]); + } + + _inAggregateInvocation = parentInAggregateInvocation; + _isCorrelatedSubquery = parentIsCorrelatedSubquery; + _tableAliasesInScope = parentTableAliasesInScope; + _aggregateArgumentContainsSubquery = parentAggregateArgumentContainsSubquery; + + return result; + } + + // We have a scalar subquery inside an aggregate function argument; lift it out to an OUTER APPLY/CROSS JOIN that will be added + // to the containing SELECT, and return a ColumnExpression in its place that references that OUTER APPLY/CROSS JOIN. + case ScalarSubqueryExpression or ExistsExpression or InExpression { Subquery: not null } + when _inAggregateInvocation && _currentSelect is not null: + _aggregateArgumentContainsSubquery = true; + return base.VisitExtension(node); + + // If _tableAliasesInScope is non-null, we're tracking which table aliases are in scope for the current subquery, to detect + // correlated vs. uncorrelated subqueries. If we have a column referencing a table that isn't in the current scope, that means + // we're in a correlated subquery. + case ColumnExpression column when _tableAliasesInScope?.Contains(column.TableAlias) == false: + _isCorrelatedSubquery = true; + return base.VisitExtension(column); + + case ShapedQueryExpression shapedQueryExpression: + shapedQueryExpression = shapedQueryExpression + .UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)) + .UpdateShaperExpression(Visit(shapedQueryExpression.ShaperExpression)); + return shapedQueryExpression.UpdateShaperExpression(Visit(shapedQueryExpression.ShaperExpression)); + + default: + return base.VisitExtension(node); + } + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs index fc4ca3b3635..a5ea2cdf6e5 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs @@ -63,18 +63,6 @@ public override bool IsBuffering || (QuerySplittingBehavior == EntityFrameworkCore.QuerySplittingBehavior.SplitQuery && !_multipleActiveResultSetsEnabled); - /// - /// Tracks whether translation is currently within the argument of an aggregate method (e.g. MAX, COUNT); SQL Server does not - /// allow subqueries and aggregates in that context. - /// - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual bool InAggregateFunction { get; set; } - /// public override bool SupportsPrecompiledQuery => true; } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs index 8fa6a96a1e4..86608b53dee 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs @@ -17,6 +17,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; public class SqlServerQueryTranslationPostprocessor : RelationalQueryTranslationPostprocessor { private readonly SqlServerJsonPostprocessor _jsonPostprocessor; + private readonly SqlServerAggregateOverSubqueryPostprocessor _aggregatePostprocessor; private readonly SkipWithoutOrderByInSplitQueryVerifier _skipWithoutOrderByInSplitQueryVerifier = new(); private readonly SqlServerSqlTreePruner _pruner = new(); @@ -34,6 +35,7 @@ public SqlServerQueryTranslationPostprocessor( { _jsonPostprocessor = new SqlServerJsonPostprocessor( relationalDependencies.TypeMappingSource, relationalDependencies.SqlExpressionFactory, queryCompilationContext.SqlAliasManager); + _aggregatePostprocessor = new SqlServerAggregateOverSubqueryPostprocessor(queryCompilationContext.SqlAliasManager); } /// @@ -47,9 +49,10 @@ public override Expression Process(Expression query) var query1 = base.Process(query); var query2 = _jsonPostprocessor.Process(query1); - _skipWithoutOrderByInSplitQueryVerifier.Visit(query2); + var query3 = _aggregatePostprocessor.Visit(query2); + _skipWithoutOrderByInSplitQueryVerifier.Visit(query3); - return query2; + return query3; } /// diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index b5abe1a98c1..9ce6e9d5425 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -120,103 +120,6 @@ protected override Expression VisitExtension(Expression extensionExpression) return base.VisitExtension(extensionExpression); } - #region Aggregate functions - - // We override these for SQL Server to add tracking whether we're inside an aggregate function context, since SQL Server doesn't - // support subqueries (or aggregates) within them. - - /// - /// 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 ShapedQueryExpression? TranslateAverage(ShapedQueryExpression source, LambdaExpression? selector, Type resultType) - { - var previousInAggregateFunction = _queryCompilationContext.InAggregateFunction; - _queryCompilationContext.InAggregateFunction = true; - var result = base.TranslateAverage(source, selector, resultType); - _queryCompilationContext.InAggregateFunction = previousInAggregateFunction; - return result; - } - - /// - /// 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 ShapedQueryExpression? TranslateSum(ShapedQueryExpression source, LambdaExpression? selector, Type resultType) - { - var previousInAggregateFunction = _queryCompilationContext.InAggregateFunction; - _queryCompilationContext.InAggregateFunction = true; - var result = base.TranslateSum(source, selector, resultType); - _queryCompilationContext.InAggregateFunction = previousInAggregateFunction; - return result; - } - - /// - /// 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 ShapedQueryExpression? TranslateCount(ShapedQueryExpression source, LambdaExpression? predicate) - { - var previousInAggregateFunction = _queryCompilationContext.InAggregateFunction; - _queryCompilationContext.InAggregateFunction = true; - var result = base.TranslateCount(source, predicate); - _queryCompilationContext.InAggregateFunction = previousInAggregateFunction; - return result; - } - - /// - /// 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 ShapedQueryExpression? TranslateLongCount(ShapedQueryExpression source, LambdaExpression? predicate) - { - var previousInAggregateFunction = _queryCompilationContext.InAggregateFunction; - _queryCompilationContext.InAggregateFunction = true; - var result = base.TranslateLongCount(source, predicate); - _queryCompilationContext.InAggregateFunction = previousInAggregateFunction; - return result; - } - - /// - /// 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 ShapedQueryExpression? TranslateMax(ShapedQueryExpression source, LambdaExpression? selector, Type resultType) - { - var previousInAggregateFunction = _queryCompilationContext.InAggregateFunction; - _queryCompilationContext.InAggregateFunction = true; - var result = base.TranslateMax(source, selector, resultType); - _queryCompilationContext.InAggregateFunction = previousInAggregateFunction; - return result; - } - - /// - /// 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 ShapedQueryExpression? TranslateMin(ShapedQueryExpression source, LambdaExpression? selector, Type resultType) - { - var previousInAggregateFunction = _queryCompilationContext.InAggregateFunction; - _queryCompilationContext.InAggregateFunction = true; - var result = base.TranslateMin(source, selector, resultType); - _queryCompilationContext.InAggregateFunction = previousInAggregateFunction; - return result; - } - - #endregion Aggregate functions - /// /// 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 @@ -407,50 +310,6 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr 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 ShapedQueryExpression? TranslateContains(ShapedQueryExpression source, Expression item) - { - var translatedSource = base.TranslateContains(source, item); - - // SQL Server does not support subqueries inside aggregate functions (e.g. COUNT(SELECT * FROM OPENJSON(@p)...)). - // As a result, we track whether we're within an aggregate function; if we are, and we see the regular Contains translation - // (which uses IN with an OPENJSON subquery - incompatible), we transform it to the old-style IN+constants translation (as if a - // low SQL Server compatibility level were defined) - if (_queryCompilationContext.InAggregateFunction - && translatedSource is not null - && TryGetProjection(translatedSource, out var projection) - && projection is InExpression - { - Item: var translatedItem, - Subquery: - { - Tables: [SqlServerOpenJsonExpression { Arguments: [SqlParameterExpression parameter] } openJsonExpression], - Predicate: null, - GroupBy: [], - Having: null, - IsDistinct: false, - Limit: null, - Offset: null, - Orderings: [], - Projection: [{ Expression: ColumnExpression { Name: "value", TableAlias: var projectionTableAlias } }] - } - } - && projectionTableAlias == openJsonExpression.Alias) - { - var newInExpression = _sqlExpressionFactory.In(translatedItem, parameter); -#pragma warning disable EF1001 - return source.UpdateQueryExpression(new SelectExpression(newInExpression, _queryCompilationContext.SqlAliasManager)); -#pragma warning restore EF1001 - } - - return translatedSource; - } - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index b09f699f206..677fe36f4ed 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -568,28 +568,6 @@ private static string EscapeLikePattern(string pattern) "LEAST", expressions, nullable: true, Enumerable.Repeat(false, expressions.Count), resultType, resultTypeMapping); } - /// - /// 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 bool TryTranslateAggregateMethodCall( - MethodCallExpression methodCallExpression, - [NotNullWhen(true)] out SqlExpression? translation) - { - var previousInAggregateFunction = _queryCompilationContext.InAggregateFunction; - _queryCompilationContext.InAggregateFunction = true; - -#pragma warning disable EF1001 // Internal EF Core API usage. - var result = base.TryTranslateAggregateMethodCall(methodCallExpression, out translation); -#pragma warning restore EF1001 // Internal EF Core API usage. - - _queryCompilationContext.InAggregateFunction = previousInAggregateFunction; - - return result; - } - private Expression TranslateByteArrayElementAccess(Expression array, Expression index, Type resultType) { var visitedArray = Visit(array); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs index d97e0bc94c6..62725434aba 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs @@ -473,26 +473,50 @@ FROM root c """); }); - public override async Task Sum_over_subquery_is_client_eval(bool async) + public override async Task Sum_over_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Sum_over_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Sum_over_subquery(async)); AssertSql(); } - public override async Task Sum_over_nested_subquery_is_client_eval(bool async) + public override async Task Sum_over_nested_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Sum_over_nested_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Sum_over_nested_subquery(async)); AssertSql(); } - public override async Task Sum_over_min_subquery_is_client_eval(bool async) + public override async Task Sum_over_min_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Sum_over_min_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Sum_over_min_subquery(async)); + + AssertSql(); + } + + public override async Task Sum_over_scalar_returning_subquery(bool async) + { + // Aggregates. Issue #16146. + await AssertTranslationFailed(() => base.Sum_over_scalar_returning_subquery(async)); + + AssertSql(); + } + + public override async Task Sum_over_Any_subquery(bool async) + { + // Aggregates. Issue #16146. + await AssertTranslationFailed(() => base.Sum_over_Any_subquery(async)); + + AssertSql(); + } + + public override async Task Sum_over_uncorrelated_subquery(bool async) + { + // Aggregates. Issue #16146. + await AssertTranslationFailed(() => base.Sum_over_uncorrelated_subquery(async)); AssertSql(); } @@ -772,26 +796,26 @@ FROM root c """); }); - public override async Task Average_over_subquery_is_client_eval(bool async) + public override async Task Average_over_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Average_over_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Average_over_subquery(async)); AssertSql(); } - public override async Task Average_over_nested_subquery_is_client_eval(bool async) + public override async Task Average_over_nested_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Average_over_nested_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Average_over_nested_subquery(async)); AssertSql(); } - public override async Task Average_over_max_subquery_is_client_eval(bool async) + public override async Task Average_over_max_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Average_over_max_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Average_over_max_subquery(async)); AssertSql(); } @@ -912,26 +936,26 @@ FROM root c """); }); - public override async Task Min_over_subquery_is_client_eval(bool async) + public override async Task Min_over_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Min_over_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Min_over_subquery(async)); AssertSql(); } - public override async Task Min_over_nested_subquery_is_client_eval(bool async) + public override async Task Min_over_nested_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Min_over_nested_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Min_over_nested_subquery(async)); AssertSql(); } - public override async Task Min_over_max_subquery_is_client_eval(bool async) + public override async Task Min_over_max_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Min_over_max_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Min_over_max_subquery(async)); AssertSql(); } @@ -978,26 +1002,26 @@ FROM root c """); }); - public override async Task Max_over_subquery_is_client_eval(bool async) + public override async Task Max_over_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Max_over_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Max_over_subquery(async)); AssertSql(); } - public override async Task Max_over_nested_subquery_is_client_eval(bool async) + public override async Task Max_over_nested_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Max_over_nested_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Max_over_nested_subquery(async)); AssertSql(); } - public override async Task Max_over_sum_subquery_is_client_eval(bool async) + public override async Task Max_over_sum_subquery(bool async) { // Aggregates. Issue #16146. - await AssertTranslationFailed(() => base.Max_over_sum_subquery_is_client_eval(async)); + await AssertTranslationFailed(() => base.Max_over_sum_subquery(async)); AssertSql(); } @@ -2060,7 +2084,7 @@ public override async Task Contains_with_local_anonymous_type_array_closure(bool public override async Task OfType_Select(bool async) { - // Contains over subquery. Issue #15937. + // Contains over subquery. Issue #17246. await AssertTranslationFailed(() => base.OfType_Select(async)); AssertSql(); diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs index 738efad295a..2b91264909c 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindAggregateOperatorsQueryInMemoryTest.cs @@ -32,6 +32,12 @@ public override async Task Average_on_nav_subquery_in_projection(bool async) (await Assert.ThrowsAsync( () => base.Average_on_nav_subquery_in_projection(async))).Message); + public override async Task Sum_over_scalar_returning_subquery(bool async) + => Assert.Equal( + "Nullable object must have a value.", + (await Assert.ThrowsAsync( + () => base.Sum_over_scalar_returning_subquery(async))).Message); + public override Task Collection_Last_member_access_in_projection_translated(bool async) => Assert.ThrowsAsync( () => base.Collection_Last_member_access_in_projection_translated(async)); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs index 0f5317eea88..1e75f6c4ca8 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs @@ -140,7 +140,7 @@ public virtual Task Sum_with_coalesce(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Sum_over_subquery_is_client_eval(bool async) + public virtual Task Sum_over_subquery(bool async) => AssertSum( async, ss => ss.Set(), @@ -148,7 +148,7 @@ public virtual Task Sum_over_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Sum_over_nested_subquery_is_client_eval(bool async) + public virtual Task Sum_over_nested_subquery(bool async) => AssertSum( async, ss => ss.Set(), @@ -156,12 +156,45 @@ public virtual Task Sum_over_nested_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Sum_over_min_subquery_is_client_eval(bool async) + public virtual Task Sum_over_min_subquery(bool async) => AssertSum( async, ss => ss.Set(), selector: c => c.Orders.Sum(o => 5 + o.OrderDetails.Min(od => od.ProductID))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Sum_over_scalar_returning_subquery(bool async) + => AssertSum( + async, + ss => ss.Set(), + ss => ss.Set(), + actualSelector: c => c.Orders.FirstOrDefault().OrderID, + expectedSelector: c => c.Orders.Any() ? c.Orders.FirstOrDefault().OrderID : 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Sum_over_Any_subquery(bool async) + => AssertSum( + async, + ss => ss.Set(), + selector: c => c.Orders.Any() ? c.Orders.FirstOrDefault().OrderID : 0); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Sum_over_uncorrelated_subquery(bool async) + { + await using var context = CreateContext(); + + // AssertSum() doesn't provide access to the ISetSource in order to do the uncorrelated query, so we test this manually. + // Note: the Count predicate is specified to work around #34261. + var result = async + ? await context.Set().SumAsync(c => context.Set().Count(o => o.OrderID > 10300)) + : context.Set().Sum(c => context.Set().Count(o => o.OrderID > 10300)); + + AssertEqual(70707, result); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Sum_on_float_column(bool async) @@ -238,7 +271,7 @@ public virtual Task Average_with_coalesce(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Average_over_subquery_is_client_eval(bool async) + public virtual Task Average_over_subquery(bool async) => AssertAverage( async, ss => ss.Set(), @@ -246,7 +279,7 @@ public virtual Task Average_over_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Average_over_nested_subquery_is_client_eval(bool async) + public virtual Task Average_over_nested_subquery(bool async) => AssertAverage( async, ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), @@ -254,7 +287,7 @@ public virtual Task Average_over_nested_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Average_over_max_subquery_is_client_eval(bool async) + public virtual Task Average_over_max_subquery(bool async) => AssertAverage( async, ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), @@ -407,7 +440,7 @@ public virtual Task Min_with_coalesce(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Min_over_subquery_is_client_eval(bool async) + public virtual Task Min_over_subquery(bool async) => AssertMin( async, ss => ss.Set(), @@ -415,7 +448,7 @@ public virtual Task Min_over_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Min_over_nested_subquery_is_client_eval(bool async) + public virtual Task Min_over_nested_subquery(bool async) => AssertMin( async, ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), @@ -423,7 +456,7 @@ public virtual Task Min_over_nested_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Min_over_max_subquery_is_client_eval(bool async) + public virtual Task Min_over_max_subquery(bool async) => AssertMin( async, ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), @@ -454,7 +487,7 @@ public virtual Task Max_with_coalesce(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Max_over_subquery_is_client_eval(bool async) + public virtual Task Max_over_subquery(bool async) => AssertMax( async, ss => ss.Set(), @@ -462,7 +495,7 @@ public virtual Task Max_over_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Max_over_nested_subquery_is_client_eval(bool async) + public virtual Task Max_over_nested_subquery(bool async) => AssertMax( async, ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), @@ -470,7 +503,7 @@ public virtual Task Max_over_nested_subquery_is_client_eval(bool async) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Max_over_sum_subquery_is_client_eval(bool async) + public virtual Task Max_over_sum_subquery(bool async) => AssertMax( async, ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index 4db5547df79..033b0de4407 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -1,7 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Xunit.Sdk; namespace Microsoft.EntityFrameworkCore.Query; @@ -700,63 +701,124 @@ WHERE [p].[ProductID] < 40 """); } - public override async Task Sum_over_subquery_is_client_eval(bool async) + public override async Task Sum_over_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Sum_over_subquery_is_client_eval(async))).Number); + await base.Sum_over_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ -SELECT COALESCE(SUM(( - SELECT COALESCE(SUM([o].[OrderID]), 0) - FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])), 0) +SELECT COALESCE(SUM([s].[value]), 0) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT COALESCE(SUM([o].[OrderID]), 0) AS [value] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [s] """); } - public override async Task Sum_over_nested_subquery_is_client_eval(bool async) + public override async Task Sum_over_nested_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Sum_over_nested_subquery_is_client_eval(async))).Number); + await base.Sum_over_nested_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ -SELECT COALESCE(SUM(( - SELECT COALESCE(SUM(5 + ( - SELECT COALESCE(SUM([o0].[ProductID]), 0) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID])), 0) +SELECT COALESCE(SUM([s0].[value]), 0) +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT COALESCE(SUM([s].[value]), 0) AS [value] FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])), 0) + OUTER APPLY ( + SELECT 5 + ( + SELECT COALESCE(SUM([o0].[ProductID]), 0) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [value] + ) AS [s] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [s0] +"""); + } + + public override async Task Sum_over_min_subquery(bool async) + { + await base.Sum_over_min_subquery(async); + + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" + AssertSql( + """ +SELECT COALESCE(SUM([s0].[value]), 0) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT COALESCE(SUM([s].[value]), 0) AS [value] + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT 5 + ( + SELECT MIN([o0].[ProductID]) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [value] + ) AS [s] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [s0] """); } - public override async Task Sum_over_min_subquery_is_client_eval(bool async) + public override async Task Sum_over_scalar_returning_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Sum_over_min_subquery_is_client_eval(async))).Number); + await base.Sum_over_scalar_returning_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ -SELECT COALESCE(SUM(( - SELECT COALESCE(SUM(5 + ( - SELECT MIN([o0].[ProductID]) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID])), 0) +SELECT COALESCE(SUM([s].[OrderID]), 0) +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT TOP(1) [o].[OrderID] FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])), 0) + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [s] +"""); + } + + public override async Task Sum_over_Any_subquery(bool async) + { + await base.Sum_over_Any_subquery(async); + + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" + AssertSql( + """ +SELECT COALESCE(SUM([s].[value]), 0) +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]) THEN ( + SELECT TOP(1) [o0].[OrderID] + FROM [Orders] AS [o0] + WHERE [c].[CustomerID] = [o0].[CustomerID]) + ELSE 0 + END AS [value] +) AS [s] +"""); + } + + public override async Task Sum_over_uncorrelated_subquery(bool async) + { + await base.Sum_over_uncorrelated_subquery(async); + + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" + AssertSql( + """ +SELECT COALESCE(SUM([s].[value]), 0) FROM [Customers] AS [c] +CROSS JOIN ( + SELECT COUNT(*) AS [value] + FROM [Orders] AS [o] + WHERE [o].[OrderID] > 10300 +) AS [s] """); } @@ -865,75 +927,87 @@ WHERE [p].[ProductID] < 40 """); } - public override async Task Average_over_subquery_is_client_eval(bool async) + public override async Task Average_over_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Average_over_subquery_is_client_eval(async))).Number); + await base.Average_over_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ -SELECT AVG(CAST(( - SELECT COALESCE(SUM([o].[OrderID]), 0) - FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID]) AS float)) +SELECT AVG([s].[value]) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT CAST(( + SELECT COALESCE(SUM([o].[OrderID]), 0) + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]) AS float) AS [value] +) AS [s] """); } - public override async Task Average_over_nested_subquery_is_client_eval(bool async) + public override async Task Average_over_nested_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Average_over_nested_subquery_is_client_eval(async))).Number); + await AssertAverage( + async, + ss => ss.Set().OrderBy(c => c.CustomerID).Take(3), + selector: c => (decimal)c.Orders.Average(o => 5 + o.OrderDetails.Average(od => od.ProductID)), + asserter: (e, a) => Assert.Equal(e, a, precision: 3)); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ @__p_0='3' -SELECT AVG(CAST(( - SELECT AVG(5.0E0 + ( - SELECT AVG(CAST([o0].[ProductID] AS float)) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID])) - FROM [Orders] AS [o] - WHERE [c0].[CustomerID] = [o].[CustomerID]) AS decimal(18,2))) +SELECT AVG([s0].[value]) FROM ( SELECT TOP(@__p_0) [c].[CustomerID] FROM [Customers] AS [c] ORDER BY [c].[CustomerID] ) AS [c0] +OUTER APPLY ( + SELECT CAST(( + SELECT AVG([s].[value]) + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT 5.0E0 + ( + SELECT AVG(CAST([o0].[ProductID] AS float)) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [value] + ) AS [s] + WHERE [c0].[CustomerID] = [o].[CustomerID]) AS decimal(18,2)) AS [value] +) AS [s0] """); } - public override async Task Average_over_max_subquery_is_client_eval(bool async) + public override async Task Average_over_max_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Average_over_max_subquery_is_client_eval(async))).Number); + // Expected: 59.841269841269866666666666667 + // Actual: 59.843333 + await Assert.ThrowsAsync(() => base.Average_over_max_subquery(async)); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ @__p_0='3' -SELECT AVG(CAST(( - SELECT AVG(CAST(5 + ( - SELECT MAX([o0].[ProductID]) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID]) AS float)) - FROM [Orders] AS [o] - WHERE [c0].[CustomerID] = [o].[CustomerID]) AS decimal(18,2))) +SELECT AVG([s0].[value]) FROM ( SELECT TOP(@__p_0) [c].[CustomerID] FROM [Customers] AS [c] ORDER BY [c].[CustomerID] ) AS [c0] +OUTER APPLY ( + SELECT CAST(( + SELECT AVG([s].[value]) + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT CAST(5 + ( + SELECT MAX([o0].[ProductID]) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS float) AS [value] + ) AS [s] + WHERE [c0].[CustomerID] = [o].[CustomerID]) AS decimal(18,2)) AS [value] +) AS [s0] """); } @@ -1013,75 +1087,78 @@ WHERE [p].[ProductID] < 40 """); } - public override async Task Min_over_subquery_is_client_eval(bool async) + public override async Task Min_over_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Min_over_subquery_is_client_eval(async))).Number); + await base.Min_over_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ -SELECT MIN(( - SELECT COALESCE(SUM([o].[OrderID]), 0) - FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])) +SELECT MIN([s].[value]) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT COALESCE(SUM([o].[OrderID]), 0) AS [value] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [s] """); } - public override async Task Min_over_nested_subquery_is_client_eval(bool async) + public override async Task Min_over_nested_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Min_over_nested_subquery_is_client_eval(async))).Number); + await base.Min_over_nested_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ @__p_0='3' -SELECT MIN(( - SELECT MIN(5 + ( - SELECT MIN([o0].[ProductID]) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID])) - FROM [Orders] AS [o] - WHERE [c0].[CustomerID] = [o].[CustomerID])) +SELECT MIN([s0].[value]) FROM ( SELECT TOP(@__p_0) [c].[CustomerID] FROM [Customers] AS [c] ORDER BY [c].[CustomerID] ) AS [c0] +OUTER APPLY ( + SELECT MIN([s].[value]) AS [value] + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT 5 + ( + SELECT MIN([o0].[ProductID]) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [value] + ) AS [s] + WHERE [c0].[CustomerID] = [o].[CustomerID] +) AS [s0] """); } - public override async Task Min_over_max_subquery_is_client_eval(bool async) + public override async Task Min_over_max_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Min_over_max_subquery_is_client_eval(async))).Number); + await base.Min_over_max_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ @__p_0='3' -SELECT MIN(( - SELECT MIN(5 + ( - SELECT MAX([o0].[ProductID]) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID])) - FROM [Orders] AS [o] - WHERE [c0].[CustomerID] = [o].[CustomerID])) +SELECT MIN([s0].[value]) FROM ( SELECT TOP(@__p_0) [c].[CustomerID] FROM [Customers] AS [c] ORDER BY [c].[CustomerID] ) AS [c0] +OUTER APPLY ( + SELECT MIN([s].[value]) AS [value] + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT 5 + ( + SELECT MAX([o0].[ProductID]) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [value] + ) AS [s] + WHERE [c0].[CustomerID] = [o].[CustomerID] +) AS [s0] """); } @@ -1119,75 +1196,78 @@ WHERE [p].[ProductID] < 40 """); } - public override async Task Max_over_subquery_is_client_eval(bool async) + public override async Task Max_over_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Max_over_subquery_is_client_eval(async))).Number); + await base.Max_over_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ -SELECT MAX(( - SELECT COALESCE(SUM([o].[OrderID]), 0) - FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])) +SELECT MAX([s].[value]) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT COALESCE(SUM([o].[OrderID]), 0) AS [value] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [s] """); } - public override async Task Max_over_nested_subquery_is_client_eval(bool async) + public override async Task Max_over_nested_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Max_over_nested_subquery_is_client_eval(async))).Number); + await base.Max_over_nested_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ @__p_0='3' -SELECT MAX(( - SELECT MAX(5 + ( - SELECT MAX([o0].[ProductID]) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID])) - FROM [Orders] AS [o] - WHERE [c0].[CustomerID] = [o].[CustomerID])) +SELECT MAX([s0].[value]) FROM ( SELECT TOP(@__p_0) [c].[CustomerID] FROM [Customers] AS [c] ORDER BY [c].[CustomerID] ) AS [c0] +OUTER APPLY ( + SELECT MAX([s].[value]) AS [value] + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT 5 + ( + SELECT MAX([o0].[ProductID]) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [value] + ) AS [s] + WHERE [c0].[CustomerID] = [o].[CustomerID] +) AS [s0] """); } - public override async Task Max_over_sum_subquery_is_client_eval(bool async) + public override async Task Max_over_sum_subquery(bool async) { - // Aggregates. Issue #15937. - Assert.Equal( - 130, - (await Assert.ThrowsAsync( - async () => await base.Max_over_sum_subquery_is_client_eval(async))).Number); + await base.Max_over_sum_subquery(async); + // #34256: rewrite query to avoid "Cannot perform an aggregate function on an expression containing an aggregate or a subquery" AssertSql( """ @__p_0='3' -SELECT MAX(( - SELECT MAX(5 + ( - SELECT COALESCE(SUM([o0].[ProductID]), 0) - FROM [Order Details] AS [o0] - WHERE [o].[OrderID] = [o0].[OrderID])) - FROM [Orders] AS [o] - WHERE [c0].[CustomerID] = [o].[CustomerID])) +SELECT MAX([s0].[value]) FROM ( SELECT TOP(@__p_0) [c].[CustomerID] FROM [Customers] AS [c] ORDER BY [c].[CustomerID] ) AS [c0] +OUTER APPLY ( + SELECT MAX([s].[value]) AS [value] + FROM [Orders] AS [o] + OUTER APPLY ( + SELECT 5 + ( + SELECT COALESCE(SUM([o0].[ProductID]), 0) + FROM [Order Details] AS [o0] + WHERE [o].[OrderID] = [o0].[OrderID]) AS [value] + ) AS [s] + WHERE [c0].[CustomerID] = [o].[CustomerID] +) AS [s0] """); } @@ -2218,7 +2298,6 @@ ELSE CAST(0 AS bit) public override async Task Contains_with_local_anonymous_type_array_closure(bool async) { - // Aggregates. Issue #15937. await AssertTranslationFailed(() => base.Contains_with_local_anonymous_type_array_closure(async)); AssertSql(); @@ -2947,10 +3026,18 @@ public override async Task Contains_inside_aggregate_function_with_GroupBy(bool AssertSql( """ -SELECT COUNT(CASE - WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 -END) +@__cities_0='["London","Berlin"]' (Size = 4000) + +SELECT COUNT([s].[value]) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT CASE + WHEN [c].[City] IN ( + SELECT [c0].[value] + FROM OPENJSON(@__cities_0) WITH ([value] nvarchar(15) '$') AS [c0] + ) THEN 1 + END AS [value] +) AS [s] GROUP BY [c].[Country] """); } @@ -2961,11 +3048,19 @@ public override async Task Contains_inside_Average_without_GroupBy(bool async) AssertSql( """ -SELECT AVG(CASE - WHEN [c].[City] IN (N'London', N'Berlin') THEN 1.0E0 - ELSE 0.0E0 -END) +@__cities_0='["London","Berlin"]' (Size = 4000) + +SELECT AVG([s].[value]) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT CASE + WHEN [c].[City] IN ( + SELECT [c0].[value] + FROM OPENJSON(@__cities_0) WITH ([value] nvarchar(15) '$') AS [c0] + ) THEN 1.0E0 + ELSE 0.0E0 + END AS [value] +) AS [s] """); } @@ -2975,11 +3070,19 @@ public override async Task Contains_inside_Sum_without_GroupBy(bool async) AssertSql( """ -SELECT COALESCE(SUM(CASE - WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 - ELSE 0 -END), 0) +@__cities_0='["London","Berlin"]' (Size = 4000) + +SELECT COALESCE(SUM([s].[value]), 0) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT CASE + WHEN [c].[City] IN ( + SELECT [c0].[value] + FROM OPENJSON(@__cities_0) WITH ([value] nvarchar(15) '$') AS [c0] + ) THEN 1 + ELSE 0 + END AS [value] +) AS [s] """); } @@ -2989,9 +3092,14 @@ public override async Task Contains_inside_Count_without_GroupBy(bool async) AssertSql( """ +@__cities_0='["London","Berlin"]' (Size = 4000) + SELECT COUNT(*) FROM [Customers] AS [c] -WHERE [c].[City] IN (N'London', N'Berlin') +WHERE [c].[City] IN ( + SELECT [c0].[value] + FROM OPENJSON(@__cities_0) WITH ([value] nvarchar(15) '$') AS [c0] +) """); } @@ -3001,9 +3109,14 @@ public override async Task Contains_inside_LongCount_without_GroupBy(bool async) AssertSql( """ +@__cities_0='["London","Berlin"]' (Size = 4000) + SELECT COUNT_BIG(*) FROM [Customers] AS [c] -WHERE [c].[City] IN (N'London', N'Berlin') +WHERE [c].[City] IN ( + SELECT [c0].[value] + FROM OPENJSON(@__cities_0) WITH ([value] nvarchar(15) '$') AS [c0] +) """); } @@ -3013,11 +3126,19 @@ public override async Task Contains_inside_Max_without_GroupBy(bool async) AssertSql( """ -SELECT MAX(CASE - WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 - ELSE 0 -END) +@__cities_0='["London","Berlin"]' (Size = 4000) + +SELECT MAX([s].[value]) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT CASE + WHEN [c].[City] IN ( + SELECT [c0].[value] + FROM OPENJSON(@__cities_0) WITH ([value] nvarchar(15) '$') AS [c0] + ) THEN 1 + ELSE 0 + END AS [value] +) AS [s] """); } @@ -3027,11 +3148,19 @@ public override async Task Contains_inside_Min_without_GroupBy(bool async) AssertSql( """ -SELECT MIN(CASE - WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 - ELSE 0 -END) +@__cities_0='["London","Berlin"]' (Size = 4000) + +SELECT MIN([s].[value]) FROM [Customers] AS [c] +OUTER APPLY ( + SELECT CASE + WHEN [c].[City] IN ( + SELECT [c0].[value] + FROM OPENJSON(@__cities_0) WITH ([value] nvarchar(15) '$') AS [c0] + ) THEN 1 + ELSE 0 + END AS [value] +) AS [s] """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs index 2c07e6a4fec..b60bc64d9ea 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs @@ -65,9 +65,9 @@ SELECT ef_avg(ef_divide(CAST("o"."Quantity" AS TEXT), '2.0')) - public override async Task Average_over_max_subquery_is_client_eval(bool async) + public override async Task Average_over_max_subquery(bool async) { - await base.Average_over_max_subquery_is_client_eval(async); + await base.Average_over_max_subquery(async); AssertSql( """ @@ -89,9 +89,9 @@ LIMIT @__p_0 """); } - public override async Task Average_over_nested_subquery_is_client_eval(bool async) + public override async Task Average_over_nested_subquery(bool async) { - await base.Average_over_nested_subquery_is_client_eval(async); + await base.Average_over_nested_subquery(async); AssertSql( """ @@ -120,7 +120,6 @@ public override async Task Multiple_collection_navigation_with_FirstOrDefault_ch () => base.Multiple_collection_navigation_with_FirstOrDefault_chained(async))).Message); public override async Task Contains_with_local_anonymous_type_array_closure(bool async) - // Aggregates. Issue #15937. => await AssertTranslationFailed(() => base.Contains_with_local_anonymous_type_array_closure(async)); public override async Task Contains_with_local_tuple_array_closure(bool async)