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