diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 7850ba29b23..a9932d09def 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -1376,7 +1376,14 @@ private StructuralTypeReferenceExpression BindComplexProperty( } } - private bool TryTranslateAggregateMethodCall( + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + protected virtual bool TryTranslateAggregateMethodCall( MethodCallExpression methodCallExpression, [NotNullWhen(true)] out SqlExpression? translation) { diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs index 98cf3777fb5..a5555d52ef7 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryCompilationContext.cs @@ -39,4 +39,16 @@ public override bool IsBuffering => base.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; } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index be2c39fcfe2..b8e45e9732c 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -18,13 +18,16 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor { - private readonly QueryCompilationContext _queryCompilationContext; + private readonly SqlServerQueryCompilationContext _queryCompilationContext; private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly int _sqlServerCompatibilityLevel; private RelationalTypeMapping? _nvarcharMaxTypeMapping; + private static readonly bool UseOldBehavior32374 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue32374", out var enabled32374) && enabled32374; + /// /// 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 @@ -34,7 +37,7 @@ public class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQu public SqlServerQueryableMethodTranslatingExpressionVisitor( QueryableMethodTranslatingExpressionVisitorDependencies dependencies, RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, - QueryCompilationContext queryCompilationContext, + SqlServerQueryCompilationContext queryCompilationContext, ISqlServerSingletonOptions sqlServerSingletonOptions) : base(dependencies, relationalDependencies, queryCompilationContext) { @@ -121,6 +124,103 @@ 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 @@ -315,6 +415,48 @@ static IEnumerable GetAllNavigationsInHierarchy(IEntityType entityT .SelectMany(t => t.GetDeclaredNavigations()); } + /// + /// 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 (!UseOldBehavior32374 + && _queryCompilationContext.InAggregateFunction + && translatedSource is not null + && TryGetProjection(translatedSource, out var projection) + && projection is InExpression + { + Item: var translatedItem, + Subquery: + { + Tables: [SqlServerOpenJsonExpression { Arguments: [SqlParameterExpression parameter] } openJsonExpression], + GroupBy: [], + Having: null, + IsDistinct: false, + Limit: null, + Offset: null, + Orderings: [], + Projection: [{ Expression: ColumnExpression { Name: "value", Table: var projectionColumnTable } }] + } + } + && projectionColumnTable == openJsonExpression) + { + var newInExpression = _sqlExpressionFactory.In(translatedItem, parameter); + return source.UpdateQueryExpression(_sqlExpressionFactory.Select(newInExpression)); + } + + 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 @@ -504,6 +646,29 @@ protected override bool IsValidSelectExpressionForExecuteUpdate( return false; } + private bool TryGetProjection(ShapedQueryExpression shapedQueryExpression, [NotNullWhen(true)] out SqlExpression? projection) + { + var shaperExpression = shapedQueryExpression.ShaperExpression; + // No need to check ConvertChecked since this is convert node which we may have added during projection + if (shaperExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression + && unaryExpression.Operand.Type.IsNullableType() + && unaryExpression.Operand.Type.UnwrapNullableType() == unaryExpression.Type) + { + shaperExpression = unaryExpression.Operand; + } + + if (shapedQueryExpression.QueryExpression is SelectExpression selectExpression + && shaperExpression is ProjectionBindingExpression projectionBindingExpression + && selectExpression.GetProjection(projectionBindingExpression) is SqlExpression sqlExpression) + { + projection = sqlExpression; + return true; + } + + projection = null; + return false; + } + private sealed class TemporalAnnotationApplyingExpressionVisitor : ExpressionVisitor { private readonly Func _annotationApplyingFunc; diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitorFactory.cs index 37389e4edf2..f8c045a5e72 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitorFactory.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitorFactory.cs @@ -49,5 +49,5 @@ public SqlServerQueryableMethodTranslatingExpressionVisitorFactory( /// public virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) => new SqlServerQueryableMethodTranslatingExpressionVisitor( - Dependencies, RelationalDependencies, queryCompilationContext, _sqlServerSingletonOptions); + Dependencies, RelationalDependencies, (SqlServerQueryCompilationContext)queryCompilationContext, _sqlServerSingletonOptions); } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index 30c3862337f..820317d2289 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -16,7 +16,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// public class SqlServerSqlTranslatingExpressionVisitor : RelationalSqlTranslatingExpressionVisitor { - private readonly QueryCompilationContext _queryCompilationContext; + private readonly SqlServerQueryCompilationContext _queryCompilationContext; private readonly ISqlExpressionFactory _sqlExpressionFactory; private static readonly HashSet DateTimeDataTypes @@ -72,7 +72,7 @@ private static readonly MethodInfo StringContainsMethodInfo /// public SqlServerSqlTranslatingExpressionVisitor( RelationalSqlTranslatingExpressionVisitorDependencies dependencies, - QueryCompilationContext queryCompilationContext, + SqlServerQueryCompilationContext queryCompilationContext, QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) : base(dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor) { @@ -430,6 +430,28 @@ private static string EscapeLikePattern(string pattern) return builder.ToString(); } + /// + /// 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/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitorFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitorFactory.cs index f46d424bde3..e038210d2cd 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitorFactory.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitorFactory.cs @@ -39,6 +39,6 @@ public virtual RelationalSqlTranslatingExpressionVisitor Create( QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) => new SqlServerSqlTranslatingExpressionVisitor( Dependencies, - queryCompilationContext, + (SqlServerQueryCompilationContext)queryCompilationContext, queryableMethodTranslatingExpressionVisitor); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs index 7fbd0ac86fd..9cdf3dc27e3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs @@ -2249,6 +2249,86 @@ public override async Task Not_Any_false(bool async) AssertSql(); } + public override async Task Contains_inside_aggregate_function_with_GroupBy(bool async) + { + // GroupBy. Issue #17313. + await AssertTranslationFailed(() => base.Contains_inside_aggregate_function_with_GroupBy(async)); + + AssertSql(); + } + + public override async Task Contains_inside_Average_without_GroupBy(bool async) + { + await base.Contains_inside_Average_without_GroupBy(async); + + AssertSql( + """ +SELECT AVG((c["City"] IN ("London", "Berlin") ? 1.0 : 0.0)) AS c +FROM root c +WHERE (c["Discriminator"] = "Customer") +"""); + } + + public override async Task Contains_inside_Sum_without_GroupBy(bool async) + { + await base.Contains_inside_Sum_without_GroupBy(async); + + AssertSql( + """ +SELECT SUM((c["City"] IN ("London", "Berlin") ? 1 : 0)) AS c +FROM root c +WHERE (c["Discriminator"] = "Customer") +"""); + } + + public override async Task Contains_inside_Count_without_GroupBy(bool async) + { + await base.Contains_inside_Count_without_GroupBy(async); + + AssertSql( + """ +SELECT COUNT(1) AS c +FROM root c +WHERE ((c["Discriminator"] = "Customer") AND c["City"] IN ("London", "Berlin")) +"""); + } + + public override async Task Contains_inside_LongCount_without_GroupBy(bool async) + { + await base.Contains_inside_LongCount_without_GroupBy(async); + + AssertSql( + """ +SELECT COUNT(1) AS c +FROM root c +WHERE ((c["Discriminator"] = "Customer") AND c["City"] IN ("London", "Berlin")) +"""); + } + + public override async Task Contains_inside_Max_without_GroupBy(bool async) + { + await base.Contains_inside_Max_without_GroupBy(async); + + AssertSql( + """ +SELECT MAX((c["City"] IN ("London", "Berlin") ? 1 : 0)) AS c +FROM root c +WHERE (c["Discriminator"] = "Customer") +"""); + } + + public override async Task Contains_inside_Min_without_GroupBy(bool async) + { + await base.Contains_inside_Min_without_GroupBy(async); + + AssertSql( + """ +SELECT MIN((c["City"] IN ("London", "Berlin") ? 1 : 0)) AS c +FROM root c +WHERE (c["Discriminator"] = "Customer") +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs index d320ac31868..9be44ba6df5 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs @@ -1923,4 +1923,89 @@ public virtual Task Not_Any_false(bool async) => AssertQuery( async, ss => ss.Set().Where(c => !c.Orders.Any(o => false)).Select(c => c.CustomerID)); + + [ConditionalTheory] // #32374 + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_inside_aggregate_function_with_GroupBy(bool async) + { + var cities = new[] { "London", "Berlin" }; + + return AssertQuery( + async, + ss => ss.Set() + .GroupBy(c => c.Country) + .Select(g => g.Count(c => cities.Contains(c.City)))); + } + + [ConditionalTheory] // #32374 + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_inside_Average_without_GroupBy(bool async) + { + var cities = new[] { "London", "Berlin" }; + + return AssertAverage( + async, + ss => ss.Set(), + selector: c => cities.Contains(c.City) ? 1.0 : 0.0); + } + + [ConditionalTheory] // #32374 + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_inside_Sum_without_GroupBy(bool async) + { + var cities = new[] { "London", "Berlin" }; + + return AssertSum( + async, + ss => ss.Set(), + selector: c => cities.Contains(c.City) ? 1 : 0); + } + + [ConditionalTheory] // #32374 + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_inside_Count_without_GroupBy(bool async) + { + var cities = new[] { "London", "Berlin" }; + + return AssertCount( + async, + ss => ss.Set(), + predicate: c => cities.Contains(c.City)); + } + + [ConditionalTheory] // #32374 + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_inside_LongCount_without_GroupBy(bool async) + { + var cities = new[] { "London", "Berlin" }; + + return AssertLongCount( + async, + ss => ss.Set(), + predicate: c => cities.Contains(c.City)); + } + + [ConditionalTheory] // #32374 + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_inside_Max_without_GroupBy(bool async) + { + var cities = new[] { "London", "Berlin" }; + + return AssertMax( + async, + ss => ss.Set(), + selector: c => cities.Contains(c.City) ? 1 : 0); + } + + [ConditionalTheory] // #32374 + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_inside_Min_without_GroupBy(bool async) + { + var cities = new[] { "London", "Berlin" }; + + return AssertMin( + async, + ss => ss.Set(), + selector: c => cities.Contains(c.City) ? 1 : 0); + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index 3f1bf17251e..cfe06b1d548 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -2902,6 +2902,100 @@ FROM [Customers] AS [c] """); } + public override async Task Contains_inside_aggregate_function_with_GroupBy(bool async) + { + await base.Contains_inside_aggregate_function_with_GroupBy(async); + + AssertSql( + """ +SELECT COUNT(CASE + WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 +END) +FROM [Customers] AS [c] +GROUP BY [c].[Country] +"""); + } + + public override async Task Contains_inside_Average_without_GroupBy(bool async) + { + await base.Contains_inside_Average_without_GroupBy(async); + + AssertSql( + """ +SELECT AVG(CASE + WHEN [c].[City] IN (N'London', N'Berlin') THEN 1.0E0 + ELSE 0.0E0 +END) +FROM [Customers] AS [c] +"""); + } + + public override async Task Contains_inside_Sum_without_GroupBy(bool async) + { + await base.Contains_inside_Sum_without_GroupBy(async); + + AssertSql( + """ +SELECT COALESCE(SUM(CASE + WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 + ELSE 0 +END), 0) +FROM [Customers] AS [c] +"""); + } + + public override async Task Contains_inside_Count_without_GroupBy(bool async) + { + await base.Contains_inside_Count_without_GroupBy(async); + + AssertSql( + """ +SELECT COUNT(*) +FROM [Customers] AS [c] +WHERE [c].[City] IN (N'London', N'Berlin') +"""); + } + + public override async Task Contains_inside_LongCount_without_GroupBy(bool async) + { + await base.Contains_inside_LongCount_without_GroupBy(async); + + AssertSql( + """ +SELECT COUNT_BIG(*) +FROM [Customers] AS [c] +WHERE [c].[City] IN (N'London', N'Berlin') +"""); + } + + public override async Task Contains_inside_Max_without_GroupBy(bool async) + { + await base.Contains_inside_Max_without_GroupBy(async); + + AssertSql( + """ +SELECT MAX(CASE + WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 + ELSE 0 +END) +FROM [Customers] AS [c] +"""); + } + + public override async Task Contains_inside_Min_without_GroupBy(bool async) + { + await base.Contains_inside_Min_without_GroupBy(async); + + AssertSql( + """ +SELECT MIN(CASE + WHEN [c].[City] IN (N'London', N'Berlin') THEN 1 + ELSE 0 +END) +FROM [Customers] AS [c] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs index cfc26454937..4e9dc50eb71 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqliteTest.cs @@ -71,4 +71,139 @@ public override async Task Contains_with_local_anonymous_type_array_closure(bool public override async Task Contains_with_local_tuple_array_closure(bool async) => await AssertTranslationFailed(() => base.Contains_with_local_tuple_array_closure(async)); + + public override async Task Contains_inside_aggregate_function_with_GroupBy(bool async) + { + await base.Contains_inside_aggregate_function_with_GroupBy(async); + + AssertSql( + """ +@__cities_0='["London","Berlin"]' (Size = 19) + +SELECT COUNT(CASE + WHEN EXISTS ( + SELECT 1 + FROM json_each(@__cities_0) AS "c0" + WHERE "c0"."value" = "c"."City" OR ("c0"."value" IS NULL AND "c"."City" IS NULL)) THEN 1 +END) +FROM "Customers" AS "c" +GROUP BY "c"."Country" +"""); + } + + public override async Task Contains_inside_Average_without_GroupBy(bool async) + { + await base.Contains_inside_Average_without_GroupBy(async); + + AssertSql( + """ +@__cities_0='["London","Berlin"]' (Size = 19) + +SELECT AVG(CASE + WHEN EXISTS ( + SELECT 1 + FROM json_each(@__cities_0) AS "c0" + WHERE "c0"."value" = "c"."City" OR ("c0"."value" IS NULL AND "c"."City" IS NULL)) THEN 1.0 + ELSE 0.0 +END) +FROM "Customers" AS "c" +"""); + } + + public override async Task Contains_inside_Sum_without_GroupBy(bool async) + { + await base.Contains_inside_Sum_without_GroupBy(async); + + AssertSql( + """ +@__cities_0='["London","Berlin"]' (Size = 19) + +SELECT COALESCE(SUM(CASE + WHEN EXISTS ( + SELECT 1 + FROM json_each(@__cities_0) AS "c0" + WHERE "c0"."value" = "c"."City" OR ("c0"."value" IS NULL AND "c"."City" IS NULL)) THEN 1 + ELSE 0 +END), 0) +FROM "Customers" AS "c" +"""); + } + + public override async Task Contains_inside_Count_without_GroupBy(bool async) + { + await base.Contains_inside_Count_without_GroupBy(async); + + AssertSql( + """ +@__cities_0='["London","Berlin"]' (Size = 19) + +SELECT COUNT(*) +FROM "Customers" AS "c" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__cities_0) AS "c0" + WHERE "c0"."value" = "c"."City" OR ("c0"."value" IS NULL AND "c"."City" IS NULL)) +"""); + } + + public override async Task Contains_inside_LongCount_without_GroupBy(bool async) + { + await base.Contains_inside_LongCount_without_GroupBy(async); + + AssertSql( + """ +@__cities_0='["London","Berlin"]' (Size = 19) + +SELECT COUNT(*) +FROM "Customers" AS "c" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__cities_0) AS "c0" + WHERE "c0"."value" = "c"."City" OR ("c0"."value" IS NULL AND "c"."City" IS NULL)) +"""); + } + + public override async Task Contains_inside_Max_without_GroupBy(bool async) + { + await base.Contains_inside_Max_without_GroupBy(async); + + AssertSql( + """ +@__cities_0='["London","Berlin"]' (Size = 19) + +SELECT MAX(CASE + WHEN EXISTS ( + SELECT 1 + FROM json_each(@__cities_0) AS "c0" + WHERE "c0"."value" = "c"."City" OR ("c0"."value" IS NULL AND "c"."City" IS NULL)) THEN 1 + ELSE 0 +END) +FROM "Customers" AS "c" +"""); + } + + public override async Task Contains_inside_Min_without_GroupBy(bool async) + { + await base.Contains_inside_Min_without_GroupBy(async); + + AssertSql( + """ +@__cities_0='["London","Berlin"]' (Size = 19) + +SELECT MIN(CASE + WHEN EXISTS ( + SELECT 1 + FROM json_each(@__cities_0) AS "c0" + WHERE "c0"."value" = "c"."City" OR ("c0"."value" IS NULL AND "c"."City" IS NULL)) THEN 1 + ELSE 0 +END) +FROM "Customers" AS "c" +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); }