From b7e5c13de585abe0786f0c59fa973620a561d439 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Wed, 2 Feb 2022 09:37:31 -0800 Subject: [PATCH] Query: Convert single result subquery comparison to null to Any operation (#27284) Resolves #26744 A better fix for #18476 Initial fix for #18476 assumed that whenever we have single result operation compared to null, it will only be true if the result of single result is default when sequence is empty. This was correct for the query in the issue tracker which had anonymous type projection. Anonymous type is never null as long as there is data, it can be only null value when default is invoked i.e. empty sequence. Hence we added optimization for that but it didn't restrict to just anonymous type. For entity type projection when entity is not nullable, the same logic holds true. This helped us translate queries which wouldn't work with entity equality due to composite key from a subquery. But optimization was incorrect for the result which can be null (nullable scalar or nullable entity) as an non-empty sequence can have first result to be null which can match. The improved fix avoids doing the unrestricted optimization during preprocessing phase. Instead we moved the logic to translation phase where we can evaluate the shape of the projection coming out subquery. Now we only apply optimization for non-nullable entity and anonymous type. Scalar comparison will work by comparing to null and nullable entity will work if entity equality covers it. It will start throwing error if composite key though earlier version possibly generated wrong results for it. --- ...yExpressionTranslatingExpressionVisitor.cs | 63 ++++++++++++ ...lationalSqlTranslatingExpressionVisitor.cs | 63 ++++++++++++ .../QueryOptimizingExpressionVisitor.cs | 45 +++++---- .../Query/GearsOfWarQueryTestBase.cs | 18 ++++ .../Query/SimpleQueryTestBase.cs | 97 +++++++++++++++++++ .../ComplexNavigationsQuerySqlServerTest.cs | 13 +-- .../Query/GearsOfWarQuerySqlServerTest.cs | 26 +++++ ...indAggregateOperatorsQuerySqlServerTest.cs | 14 +-- ...orthwindMiscellaneousQuerySqlServerTest.cs | 28 +++--- .../Query/NorthwindWhereQuerySqlServerTest.cs | 12 +-- .../Query/QueryBugsTest.cs | 6 +- .../Query/SimpleQuerySqlServerTest.cs | 39 ++++++++ .../Query/TPTGearsOfWarQuerySqlServerTest.cs | 30 ++++++ .../TemporalGearsOfWarQuerySqlServerTest.cs | 26 +++++ 14 files changed, 426 insertions(+), 54 deletions(-) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 4bb2adf8ec9..865fa04f540 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -32,6 +32,24 @@ public class InMemoryExpressionTranslatingExpressionVisitor : ExpressionVisitor { private const string RuntimeParameterPrefix = QueryCompilationContext.QueryParameterPrefix + "entity_equality_"; + private static readonly List _singleResultMethodInfos = new() + { + QueryableMethods.FirstWithPredicate, + QueryableMethods.FirstWithoutPredicate, + QueryableMethods.FirstOrDefaultWithPredicate, + QueryableMethods.FirstOrDefaultWithoutPredicate, + QueryableMethods.SingleWithPredicate, + QueryableMethods.SingleWithoutPredicate, + QueryableMethods.SingleOrDefaultWithPredicate, + QueryableMethods.SingleOrDefaultWithoutPredicate, + QueryableMethods.LastWithPredicate, + QueryableMethods.LastWithoutPredicate, + QueryableMethods.LastOrDefaultWithPredicate, + QueryableMethods.LastOrDefaultWithoutPredicate + //QueryableMethodProvider.ElementAtMethodInfo, + //QueryableMethodProvider.ElementAtOrDefaultMethodInfo + }; + private static readonly MemberInfo _valueBufferIsEmpty = typeof(ValueBuffer).GetMember(nameof(ValueBuffer.IsEmpty))[0]; private static readonly MethodInfo _parameterValueExtractor = @@ -161,6 +179,51 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) return Visit(ConvertObjectArrayEqualityComparison(binaryExpression.Left, binaryExpression.Right)); } + if (!(AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue26744", out var enabled) && enabled)) + { + if ((binaryExpression.NodeType == ExpressionType.Equal || binaryExpression.NodeType == ExpressionType.NotEqual) + && (binaryExpression.Left.IsNullConstantExpression() || binaryExpression.Right.IsNullConstantExpression())) + { + var nonNullExpression = binaryExpression.Left.IsNullConstantExpression() ? binaryExpression.Right : binaryExpression.Left; + if (nonNullExpression is MethodCallExpression nonNullMethodCallExpression + && nonNullMethodCallExpression.Method.DeclaringType == typeof(Queryable) + && nonNullMethodCallExpression.Method.IsGenericMethod + && _singleResultMethodInfos.Contains(nonNullMethodCallExpression.Method.GetGenericMethodDefinition())) + { + var source = nonNullMethodCallExpression.Arguments[0]; + if (nonNullMethodCallExpression.Arguments.Count == 2) + { + source = Expression.Call( + QueryableMethods.Where.MakeGenericMethod(source.Type.GetSequenceType()), + source, + nonNullMethodCallExpression.Arguments[1]); + } + + var translatedSubquery = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(source); + if (translatedSubquery != null) + { + var projection = translatedSubquery.ShaperExpression; + if (projection is NewExpression + || RemoveConvert(projection) is EntityShaperExpression { IsNullable: false }) + { + var anySubquery = Expression.Call( + QueryableMethods.AnyWithoutPredicate.MakeGenericMethod(translatedSubquery.Type.GetSequenceType()), + translatedSubquery); + + return Visit(binaryExpression.NodeType == ExpressionType.Equal + ? Expression.Not(anySubquery) + : anySubquery); + } + + static Expression RemoveConvert(Expression e) + => e is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary + ? RemoveConvert(unary.Operand) + : e; + } + } + } + } + var newLeft = Visit(binaryExpression.Left); var newRight = Visit(binaryExpression.Right); diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index be5a7eb3d7d..73b09d7142f 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -31,6 +31,24 @@ public class RelationalSqlTranslatingExpressionVisitor : ExpressionVisitor { private const string RuntimeParameterPrefix = QueryCompilationContext.QueryParameterPrefix + "entity_equality_"; + private static readonly List _singleResultMethodInfos = new() + { + QueryableMethods.FirstWithPredicate, + QueryableMethods.FirstWithoutPredicate, + QueryableMethods.FirstOrDefaultWithPredicate, + QueryableMethods.FirstOrDefaultWithoutPredicate, + QueryableMethods.SingleWithPredicate, + QueryableMethods.SingleWithoutPredicate, + QueryableMethods.SingleOrDefaultWithPredicate, + QueryableMethods.SingleOrDefaultWithoutPredicate, + QueryableMethods.LastWithPredicate, + QueryableMethods.LastWithoutPredicate, + QueryableMethods.LastOrDefaultWithPredicate, + QueryableMethods.LastOrDefaultWithoutPredicate + //QueryableMethodProvider.ElementAtMethodInfo, + //QueryableMethodProvider.ElementAtOrDefaultMethodInfo + }; + private static readonly MethodInfo _parameterValueExtractor = typeof(RelationalSqlTranslatingExpressionVisitor).GetRequiredDeclaredMethod(nameof(ParameterValueExtractor)); @@ -324,6 +342,51 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) right = rightOperand!; } + if (!(AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue26744", out var enabled) && enabled)) + { + if ((binaryExpression.NodeType == ExpressionType.Equal || binaryExpression.NodeType == ExpressionType.NotEqual) + && (left.IsNullConstantExpression() || right.IsNullConstantExpression())) + { + var nonNullExpression = left.IsNullConstantExpression() ? right : left; + if (nonNullExpression is MethodCallExpression nonNullMethodCallExpression + && nonNullMethodCallExpression.Method.DeclaringType == typeof(Queryable) + && nonNullMethodCallExpression.Method.IsGenericMethod + && _singleResultMethodInfos.Contains(nonNullMethodCallExpression.Method.GetGenericMethodDefinition())) + { + var source = nonNullMethodCallExpression.Arguments[0]; + if (nonNullMethodCallExpression.Arguments.Count == 2) + { + source = Expression.Call( + QueryableMethods.Where.MakeGenericMethod(source.Type.GetSequenceType()), + source, + nonNullMethodCallExpression.Arguments[1]); + } + + var translatedSubquery = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery(source); + if (translatedSubquery != null) + { + var projection = translatedSubquery.ShaperExpression; + if (projection is NewExpression + || RemoveConvert(projection) is EntityShaperExpression { IsNullable: false }) + { + var anySubquery = Expression.Call( + QueryableMethods.AnyWithoutPredicate.MakeGenericMethod(translatedSubquery.Type.GetSequenceType()), + translatedSubquery); + + return Visit(binaryExpression.NodeType == ExpressionType.Equal + ? Expression.Not(anySubquery) + : anySubquery); + } + + static Expression RemoveConvert(Expression e) + => e is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unary + ? RemoveConvert(unary.Operand) + : e; + } + } + } + } + var visitedLeft = Visit(left); var visitedRight = Visit(right); diff --git a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs index d1463185cb2..69b3abb13b0 100644 --- a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs @@ -77,30 +77,33 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) } } - if (binaryExpression.NodeType == ExpressionType.Equal - || binaryExpression.NodeType == ExpressionType.NotEqual) + if (AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue26744", out var enabled) && enabled) { - var leftNullConstant = IsNullConstant(left); - var rightNullConstant = IsNullConstant(right); - if (leftNullConstant || rightNullConstant) + if (binaryExpression.NodeType == ExpressionType.Equal + || binaryExpression.NodeType == ExpressionType.NotEqual) { - var nonNullExpression = leftNullConstant ? right : left; - if (nonNullExpression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.DeclaringType == typeof(Queryable) - && methodCallExpression.Method.IsGenericMethod - && methodCallExpression.Method.GetGenericMethodDefinition() is MethodInfo genericMethod - && _singleResultMethodInfos.Contains(genericMethod)) + var leftNullConstant = IsNullConstant(left); + var rightNullConstant = IsNullConstant(right); + if (leftNullConstant || rightNullConstant) { - var result = Expression.Call( - (methodCallExpression.Arguments.Count == 2 - ? QueryableMethods.AnyWithPredicate - : QueryableMethods.AnyWithoutPredicate) - .MakeGenericMethod(methodCallExpression.Type), - methodCallExpression.Arguments); - - return binaryExpression.NodeType == ExpressionType.Equal - ? Expression.Not(result) - : result; + var nonNullExpression = leftNullConstant ? right : left; + if (nonNullExpression is MethodCallExpression methodCallExpression + && methodCallExpression.Method.DeclaringType == typeof(Queryable) + && methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.GetGenericMethodDefinition() is MethodInfo genericMethod + && _singleResultMethodInfos.Contains(genericMethod)) + { + var result = Expression.Call( + (methodCallExpression.Arguments.Count == 2 + ? QueryableMethods.AnyWithPredicate + : QueryableMethods.AnyWithoutPredicate) + .MakeGenericMethod(methodCallExpression.Type), + methodCallExpression.Arguments); + + return binaryExpression.NodeType == ExpressionType.Equal + ? Expression.Not(result) + : result; + } } } } diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index a6ea1fc153a..cb18ba26dff 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -9138,6 +9138,24 @@ public virtual Task Where_equals_method_on_nullable_with_object_overload(bool as ss => ss.Set().Where(m => m.Rating.Equals(null))); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_subquery_equality_to_null_with_composite_key(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(s => s.Members.OrderBy(e => e.Nickname).FirstOrDefault() == null)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_subquery_equality_to_null_without_composite_key(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(s => s.Weapons.OrderBy(e => e.Name).FirstOrDefault() == null)); + } + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs index d3e4cfed26e..9b282fb5d67 100644 --- a/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.cs @@ -889,5 +889,102 @@ public class ChildFilter2 public string Filter2 { get; set; } public string Value2 { get; set; } } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Subquery_first_member_compared_to_null(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + using var context = contextFactory.CreateContext(); + + var query = context.Parents + .Where(p => p.Children.Any(c => c.SomeNullableDateTime == null) + && p.Children.Where(c => c.SomeNullableDateTime == null) + .OrderBy(c => c.SomeInteger) + .First().SomeOtherNullableDateTime != null) + .Select(p => p.Children.Where(c => c.SomeNullableDateTime == null) + .OrderBy(c => c.SomeInteger) + .First().SomeOtherNullableDateTime); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Single(result); + } + + [ConditionalTheory(Skip = "Issue#26756")] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SelectMany_where_Select(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + using var context = contextFactory.CreateContext(); + + var query = context.Parents + .SelectMany(p => p.Children + .Where(c => c.SomeNullableDateTime == null) + .OrderBy(c => c.SomeInteger) + .Take(1)) + .Where(c => c.SomeOtherNullableDateTime != null) + .Select(c => c.SomeNullableDateTime); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Single(result); + } + + protected class Context26744 : DbContext + { + public Context26744(DbContextOptions options) + : base(options) + { + } + + public DbSet Parents { get; set; } + public void Seed() + { + Add(new Parent26744 + { + Children = new List + { + new Child26744 + { + SomeInteger = 1, + SomeOtherNullableDateTime = new DateTime(2000, 11, 18) + } + } + }); + + Add(new Parent26744 + { + Children = new List + { + new Child26744 + { + SomeInteger = 1, + } + } + }); + + SaveChanges(); + } + } + + protected class Parent26744 + { + public int Id { get; set; } + public List Children { get; set; } + } + + protected class Child26744 + { + public int Id { get; set; } + public int SomeInteger { get; set; } + public DateTime? SomeNullableDateTime { get; set; } + public DateTime? SomeOtherNullableDateTime { get; set; } + public Parent26744 Parent { get; set; } + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 56a38d106d6..ebe6285d6fb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -3213,10 +3213,11 @@ public override async Task Member_pushdown_with_multiple_collections(bool async) @"SELECT ( SELECT TOP(1) [l0].[Name] FROM [LevelThree] AS [l0] - WHERE EXISTS ( - SELECT 1 + WHERE (( + SELECT TOP(1) [l1].[Id] FROM [LevelTwo] AS [l1] - WHERE [l].[Id] = [l1].[OneToMany_Optional_Inverse2Id]) AND ((( + WHERE [l].[Id] = [l1].[OneToMany_Optional_Inverse2Id] + ORDER BY [l1].[Id]) IS NOT NULL) AND ((( SELECT TOP(1) [l2].[Id] FROM [LevelTwo] AS [l2] WHERE [l].[Id] = [l2].[OneToMany_Optional_Inverse2Id] @@ -3699,10 +3700,10 @@ public override async Task Multiple_collection_FirstOrDefault_followed_by_member @"SELECT [l].[Id], ( SELECT TOP(1) [l0].[Name] FROM [LevelThree] AS [l0] - WHERE EXISTS ( - SELECT 1 + WHERE (( + SELECT TOP(1) [l1].[Id] FROM [LevelTwo] AS [l1] - WHERE ([l].[Id] = [l1].[OneToMany_Optional_Inverse2Id]) AND ([l1].[Name] = N'L2 02')) AND ((( + WHERE ([l].[Id] = [l1].[OneToMany_Optional_Inverse2Id]) AND ([l1].[Name] = N'L2 02')) IS NOT NULL) AND ((( SELECT TOP(1) [l2].[Id] FROM [LevelTwo] AS [l2] WHERE ([l].[Id] = [l2].[OneToMany_Optional_Inverse2Id]) AND ([l2].[Name] = N'L2 02')) = [l0].[OneToMany_Optional_Inverse3Id]) OR ((( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 28b175d49f6..d76f3a028fa 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -8223,6 +8223,32 @@ OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY ORDER BY [t0].[Nickname], [t0].[SquadId], [t0].[HasSoulPatch0]"); } + public override async Task Where_subquery_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_equality_to_null_with_composite_key(async); + + AssertSql( + @"SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Gears] AS [g] + WHERE [s].[Id] = [g].[SquadId]))"); + } + + public override async Task Where_subquery_equality_to_null_without_composite_key(bool async) + { + await base.Where_subquery_equality_to_null_without_composite_key(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] +FROM [Gears] AS [g] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName]))"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index c3b4b4fdc72..da7bff65e47 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -591,10 +591,11 @@ FROM [Customers] AS [c] OUTER APPLY ( SELECT TOP(1) [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] FROM [Order Details] AS [o] - WHERE EXISTS ( - SELECT 1 + WHERE (( + SELECT TOP(1) [o0].[OrderID] FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID]) AND (( + WHERE [c].[CustomerID] = [o0].[CustomerID] + ORDER BY [o0].[OrderID]) IS NOT NULL) AND (( SELECT TOP(1) [o1].[OrderID] FROM [Orders] AS [o1] WHERE [c].[CustomerID] = [o1].[CustomerID] @@ -613,10 +614,11 @@ public override async Task Multiple_collection_navigation_with_FirstOrDefault_ch @"SELECT ( SELECT TOP(1) [o].[ProductID] FROM [Order Details] AS [o] - WHERE EXISTS ( - SELECT 1 + WHERE (( + SELECT TOP(1) [o0].[OrderID] FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID]) AND (( + WHERE [c].[CustomerID] = [o0].[CustomerID] + ORDER BY [o0].[OrderID]) IS NOT NULL) AND (( SELECT TOP(1) [o1].[OrderID] FROM [Orders] AS [o1] WHERE [c].[CustomerID] = [o1].[CustomerID] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index c01dc422a63..cc0ab6a34cc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -3889,10 +3889,11 @@ public override async Task Subquery_is_null_translated_correctly(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE NOT (EXISTS ( - SELECT 1 +WHERE ( + SELECT TOP(1) [o].[CustomerID] FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID]))"); + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderID] DESC) IS NULL"); } public override async Task Subquery_is_not_null_translated_correctly(bool async) @@ -3902,10 +3903,11 @@ public override async Task Subquery_is_not_null_translated_correctly(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE EXISTS ( - SELECT 1 +WHERE ( + SELECT TOP(1) [o].[CustomerID] FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])"); + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderID] DESC) IS NOT NULL"); } public override async Task Select_take_average(bool async) @@ -4601,11 +4603,12 @@ public override async Task Dependent_to_principal_navigation_equal_to_null_for_s AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE NOT (EXISTS ( - SELECT 1 +WHERE ( + SELECT TOP(1) [c0].[CustomerID] FROM [Orders] AS [o] LEFT JOIN [Customers] AS [c0] ON [o].[CustomerID] = [c0].[CustomerID] - WHERE [c].[CustomerID] = [o].[CustomerID]))"); + WHERE [c].[CustomerID] = [o].[CustomerID] + ORDER BY [o].[OrderID]) IS NULL"); } public override async Task Collection_navigation_equality_rewrite_for_subquery(bool async) @@ -5128,11 +5131,12 @@ public override async Task Pending_selector_in_cardinality_reducing_method_is_ap WHEN EXISTS ( SELECT 1 FROM [Orders] AS [o] - WHERE (EXISTS ( - SELECT 1 + WHERE ((( + SELECT TOP(1) [c0].[CustomerID] FROM [Orders] AS [o0] LEFT JOIN [Customers] AS [c0] ON [o0].[CustomerID] = [c0].[CustomerID] - WHERE [c].[CustomerID] = [o0].[CustomerID]) AND ((( + WHERE [c].[CustomerID] = [o0].[CustomerID] + ORDER BY [o0].[OrderDate]) IS NOT NULL) AND ((( SELECT TOP(1) [c1].[CustomerID] FROM [Orders] AS [o1] LEFT JOIN [Customers] AS [c1] ON [o1].[CustomerID] = [c1].[CustomerID] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index bc0640a0c4e..58dd3aebbfe 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -2295,10 +2295,10 @@ public override async Task FirstOrDefault_over_scalar_projection_compared_to_nul AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE NOT (EXISTS ( - SELECT 1 +WHERE ( + SELECT TOP(1) [o].[OrderID] FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID]))"); + WHERE [c].[CustomerID] = [o].[CustomerID]) IS NULL"); } public override async Task FirstOrDefault_over_scalar_projection_compared_to_not_null(bool async) @@ -2308,10 +2308,10 @@ public override async Task FirstOrDefault_over_scalar_projection_compared_to_not AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE EXISTS ( - SELECT 1 +WHERE ( + SELECT TOP(1) [o].[OrderID] FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])"); + WHERE [c].[CustomerID] = [o].[CustomerID]) IS NOT NULL"); } public override async Task FirstOrDefault_over_custom_projection_compared_to_null(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 172cf20e26b..bdcab3d9992 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -5348,10 +5348,10 @@ OUTER APPLY ( SELECT [s].[ThingId], [t].[Id], [s].[Id] AS [Id0] FROM [Things] AS [t] LEFT JOIN [Subthings] AS [s] ON [t].[Id] = [s].[ThingId] - WHERE EXISTS ( - SELECT 1 + WHERE (( + SELECT TOP(1) [v].[Id] FROM [Values] AS [v] - WHERE [e].[Id] = [v].[Entity11023Id]) AND ((( + WHERE [e].[Id] = [v].[Entity11023Id]) IS NOT NULL) AND ((( SELECT TOP(1) [v0].[Id] FROM [Values] AS [v0] WHERE [e].[Id] = [v0].[Entity11023Id]) = [t].[Value11023Id]) OR ((( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs index da76b8115e4..895f391f933 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs @@ -257,5 +257,44 @@ FROM [ChildFilter1] AS [c] ) AS [t0] ON [t].[ChildFilter1Id] = [t0].[Id] GROUP BY [t].[Key]"); } + + public override async Task Subquery_first_member_compared_to_null(bool async) + { + await base.Subquery_first_member_compared_to_null(async); + + AssertSql( + @"SELECT ( + SELECT TOP(1) [c1].[SomeOtherNullableDateTime] + FROM [Child26744] AS [c1] + WHERE ([p].[Id] = [c1].[ParentId]) AND ([c1].[SomeNullableDateTime] IS NULL) + ORDER BY [c1].[SomeInteger]) +FROM [Parents] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM [Child26744] AS [c] + WHERE ([p].[Id] = [c].[ParentId]) AND ([c].[SomeNullableDateTime] IS NULL)) AND (( + SELECT TOP(1) [c0].[SomeOtherNullableDateTime] + FROM [Child26744] AS [c0] + WHERE ([p].[Id] = [c0].[ParentId]) AND ([c0].[SomeNullableDateTime] IS NULL) + ORDER BY [c0].[SomeInteger]) IS NOT NULL)"); + } + + public override async Task SelectMany_where_Select(bool async) + { + await base.SelectMany_where_Select(async); + + AssertSql( + @"SELECT [t0].[SomeNullableDateTime] +FROM [Parents] AS [p] +INNER JOIN ( + SELECT [t].[ParentId], [t].[SomeNullableDateTime], [t].[SomeOtherNullableDateTime] + FROM ( + SELECT [c].[ParentId], [c].[SomeNullableDateTime], [c].[SomeOtherNullableDateTime], ROW_NUMBER() OVER(PARTITION BY [c].[ParentId], [c].[SomeNullableDateTime] ORDER BY [c].[SomeInteger]) AS [row] + FROM [Child] AS [c] + ) AS [t] + WHERE [t].[row] <= 1 +) AS [t0] ON ([p].[Id] = [t0].[ParentId]) AND [t0].[SomeNullableDateTime] IS NULL +WHERE [t0].[SomeOtherNullableDateTime] IS NOT NULL"); + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 260221afe16..e8da09c4967 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -9173,6 +9173,36 @@ OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY ORDER BY [t0].[Nickname], [t0].[SquadId], [t0].[HasSoulPatch0]"); } + public override async Task Where_subquery_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_equality_to_null_with_composite_key(async); + + AssertSql( + @"SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name] +FROM [Squads] AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Gears] AS [g] + LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) + WHERE [s].[Id] = [g].[SquadId]))"); + } + + public override async Task Where_subquery_equality_to_null_without_composite_key(bool async) + { + await base.Where_subquery_equality_to_null_without_composite_key(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE + WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' +END AS [Discriminator] +FROM [Gears] AS [g] +LEFT JOIN [Officers] AS [o] ON ([g].[Nickname] = [o].[Nickname]) AND ([g].[SquadId] = [o].[SquadId]) +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName]))"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index fe1586eec00..df56b10d1a0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -385,6 +385,32 @@ public virtual async Task Set_operation_on_temporal_different_dates(bool async) Assert.Equal(SqlServerStrings.TemporalSetOperationOnMismatchedSources(nameof(Gear)), message); } + public override async Task Where_subquery_equality_to_null_with_composite_key(bool async) + { + await base.Where_subquery_equality_to_null_with_composite_key(async); + + AssertSql( + @"SELECT [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name], [s].[PeriodEnd], [s].[PeriodStart] +FROM [Squads] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] + WHERE [s].[Id] = [g].[SquadId]))"); + } + + public override async Task Where_subquery_equality_to_null_without_composite_key(bool async) + { + await base.Where_subquery_equality_to_null_without_composite_key(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName]))"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }