diff --git a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs index 4914320cd95..226a4310bdc 100644 --- a/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryOptimizingExpressionVisitor.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.EntityFrameworkCore.Internal; @@ -18,6 +20,24 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal /// public class QueryOptimizingExpressionVisitor : ExpressionVisitor { + 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 _stringCompareWithComparisonMethod = typeof(string).GetRequiredRuntimeMethod(nameof(string.Compare), new[] { typeof(string), typeof(string), typeof(StringComparison) }); @@ -32,6 +52,128 @@ public class QueryOptimizingExpressionVisitor : ExpressionVisitor private static readonly Expression _constantNullString = Expression.Constant(null, typeof(string)); + /// + /// 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 VisitBinary(BinaryExpression binaryExpression) + { + var left = Visit(binaryExpression.Left); + var right = Visit(binaryExpression.Right); + + if (binaryExpression.NodeType != ExpressionType.Coalesce + && left.Type != right.Type + && left.Type.UnwrapNullableType() == right.Type.UnwrapNullableType()) + { + if (left.Type.IsNullableValueType()) + { + right = Expression.Convert(right, left.Type); + } + else + { + left = Expression.Convert(left, right.Type); + } + } + + if (binaryExpression.NodeType == ExpressionType.Equal + || binaryExpression.NodeType == ExpressionType.NotEqual) + { + var leftNullConstant = IsNullConstant(left); + var rightNullConstant = IsNullConstant(right); + if (leftNullConstant || rightNullConstant) + { + 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; + } + } + } + + return binaryExpression.Update(left, binaryExpression.Conversion, right); + } + + /// + /// 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 VisitConditional(ConditionalExpression conditionalExpression) + { + var test = Visit(conditionalExpression.Test); + var ifTrue = Visit(conditionalExpression.IfTrue); + var ifFalse = Visit(conditionalExpression.IfFalse); + + if (ifTrue.Type != ifFalse.Type + && ifTrue.Type.UnwrapNullableType() == ifFalse.Type.UnwrapNullableType()) + { + if (ifTrue.Type.IsNullableValueType()) + { + ifFalse = Expression.Convert(ifFalse, ifTrue.Type); + } + else + { + ifTrue = Expression.Convert(ifTrue, ifFalse.Type); + } + + return Expression.Condition(test, ifTrue, ifFalse); + } + + return conditionalExpression.Update(test, ifTrue, ifFalse); + } + + /// + /// 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 ElementInit VisitElementInit(ElementInit elementInit) + { + var arguments = new Expression[elementInit.Arguments.Count]; + for (var i = 0; i < arguments.Length; i++) + { + arguments[i] = MatchExpressionType( + Visit(elementInit.Arguments[i]), + elementInit.Arguments[i].Type); + } + + return elementInit.Update(arguments); + } + + /// + /// 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 VisitLambda(Expression lambdaExpression) + { + Check.NotNull(lambdaExpression, nameof(lambdaExpression)); + + var body = Visit(lambdaExpression.Body); + + return body.Type != lambdaExpression.Body.Type + ? Expression.Lambda(Expression.Convert(body, lambdaExpression.Body.Type), lambdaExpression.Parameters) + : lambdaExpression.Update(body, lambdaExpression.Parameters)!; + } + /// /// 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 @@ -51,6 +193,21 @@ protected override Expression VisitMember(MemberExpression memberExpression) return TryOptimizeMemberAccessOverConditional(visitedExpression) ?? visitedExpression; } + /// + /// 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 MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment) + { + var expression = MatchExpressionType( + Visit(memberAssignment.Expression), + memberAssignment.Expression.Type); + + return memberAssignment.Update(expression); + } + /// /// 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 @@ -136,16 +293,14 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp if (methodCallExpression.Object != null) { @object = MatchExpressionType( - Visit(methodCallExpression.Object), - methodCallExpression.Object.Type); + Visit(methodCallExpression.Object), methodCallExpression.Object.Type); } var arguments = new Expression[methodCallExpression.Arguments.Count]; for (var i = 0; i < arguments.Length; i++) { arguments[i] = MatchExpressionType( - Visit(methodCallExpression.Arguments[i]), - methodCallExpression.Arguments[i].Type); + Visit(methodCallExpression.Arguments[i]), methodCallExpression.Arguments[i].Type); } var visited = methodCallExpression.Update(@object!, arguments); @@ -177,131 +332,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return visited; } - private Expression MatchExpressionType(Expression expression, Type typeToMatch) - => expression.Type != typeToMatch - ? Expression.Convert(expression, typeToMatch) - : expression; - - /// - /// 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 VisitUnary(UnaryExpression unaryExpression) - { - Check.NotNull(unaryExpression, nameof(unaryExpression)); - - if (unaryExpression.NodeType == ExpressionType.Not - && unaryExpression.Operand is MethodCallExpression innerMethodCall - && (Equals(_startsWithMethodInfo, innerMethodCall.Method) - || Equals(_endsWithMethodInfo, innerMethodCall.Method))) - { - if (innerMethodCall.Arguments[0] is ConstantExpression constantArgument - && constantArgument.Value is string stringValue - && stringValue == string.Empty) - { - // every string starts/ends with empty string. - return Expression.Constant(false); - } - - var newObject = Visit(innerMethodCall.Object)!; - var newArgument = Visit(innerMethodCall.Arguments[0]); - - var result = Expression.AndAlso( - Expression.NotEqual(newObject, _constantNullString), - Expression.AndAlso( - Expression.NotEqual(newArgument, _constantNullString), - Expression.Not(innerMethodCall.Update(newObject, new[] { newArgument })))); - - return newArgument is ConstantExpression - ? result - : Expression.AndAlso( - Expression.NotEqual( - newArgument, - Expression.Constant(string.Empty)), - result); - } - - return unaryExpression.Update( - Visit(unaryExpression.Operand)); - } - - /// - /// 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 VisitBinary(BinaryExpression binaryExpression) - { - var left = Visit(binaryExpression.Left); - var right = Visit(binaryExpression.Right); - - if (binaryExpression.NodeType != ExpressionType.Coalesce - && left.Type != right.Type - && left.Type.UnwrapNullableType() == right.Type.UnwrapNullableType()) - { - if (left.Type.IsNullableValueType()) - { - right = Expression.Convert(right, left.Type); - } - else - { - left = Expression.Convert(left, right.Type); - } - } - - return binaryExpression.Update(left, binaryExpression.Conversion, right); - } - - /// - /// 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 VisitConditional(ConditionalExpression conditionalExpression) - { - var test = Visit(conditionalExpression.Test); - var ifTrue = Visit(conditionalExpression.IfTrue); - var ifFalse = Visit(conditionalExpression.IfFalse); - - if (ifTrue.Type != ifFalse.Type - && ifTrue.Type.UnwrapNullableType() == ifFalse.Type.UnwrapNullableType()) - { - if (ifTrue.Type.IsNullableValueType()) - { - ifFalse = Expression.Convert(ifFalse, ifTrue.Type); - } - else - { - ifTrue = Expression.Convert(ifTrue, ifFalse.Type); - } - - return Expression.Condition(test, ifTrue, ifFalse); - } - - return conditionalExpression.Update(test, ifTrue, ifFalse); - } - - /// - /// 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 VisitLambda(Expression lambdaExpression) - { - Check.NotNull(lambdaExpression, nameof(lambdaExpression)); - - var body = Visit(lambdaExpression.Body); - - return body.Type != lambdaExpression.Body.Type - ? Expression.Lambda(Expression.Convert(body, lambdaExpression.Body.Type), lambdaExpression.Parameters) - : lambdaExpression.Update(body, lambdaExpression.Parameters)!; - } - /// /// 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 @@ -332,17 +362,17 @@ protected override Expression VisitNew(NewExpression newExpression) /// 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 ElementInit VisitElementInit(ElementInit elementInit) + protected override Expression VisitNewArray(NewArrayExpression newArrayExpression) { - var arguments = new Expression[elementInit.Arguments.Count]; - for (var i = 0; i < arguments.Length; i++) + var expressions = new Expression[newArrayExpression.Expressions.Count]; + for (var i = 0; i < expressions.Length; i++) { - arguments[i] = MatchExpressionType( - Visit(elementInit.Arguments[i]), - elementInit.Arguments[i].Type); + expressions[i] = MatchExpressionType( + Visit(newArrayExpression.Expressions[i]), + newArrayExpression.Expressions[i].Type); } - return elementInit.Update(arguments); + return newArrayExpression.Update(expressions); } /// @@ -351,34 +381,50 @@ protected override ElementInit VisitElementInit(ElementInit elementInit) /// 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 MemberAssignment VisitMemberAssignment(MemberAssignment memberAssignment) + protected override Expression VisitUnary(UnaryExpression unaryExpression) { - var expression = MatchExpressionType( - Visit(memberAssignment.Expression), - memberAssignment.Expression.Type); - - return memberAssignment.Update(expression); - } + Check.NotNull(unaryExpression, nameof(unaryExpression)); - /// - /// 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 VisitNewArray(NewArrayExpression newArrayExpression) - { - var expressions = new Expression[newArrayExpression.Expressions.Count]; - for (var i = 0; i < expressions.Length; i++) + if (unaryExpression.NodeType == ExpressionType.Not + && unaryExpression.Operand is MethodCallExpression innerMethodCall + && (Equals(_startsWithMethodInfo, innerMethodCall.Method) + || Equals(_endsWithMethodInfo, innerMethodCall.Method))) { - expressions[i] = MatchExpressionType( - Visit(newArrayExpression.Expressions[i]), - newArrayExpression.Expressions[i].Type); + if (innerMethodCall.Arguments[0] is ConstantExpression constantArgument + && constantArgument.Value is string stringValue + && stringValue == string.Empty) + { + // every string starts/ends with empty string. + return Expression.Constant(false); + } + + var newObject = Visit(innerMethodCall.Object)!; + var newArgument = Visit(innerMethodCall.Arguments[0]); + + var result = Expression.AndAlso( + Expression.NotEqual(newObject, _constantNullString), + Expression.AndAlso( + Expression.NotEqual(newArgument, _constantNullString), + Expression.Not(innerMethodCall.Update(newObject, new[] { newArgument })))); + + return newArgument is ConstantExpression + ? result + : Expression.AndAlso( + Expression.NotEqual( + newArgument, + Expression.Constant(string.Empty)), + result); } - return newArrayExpression.Update(expressions); + return unaryExpression.Update( + Visit(unaryExpression.Operand)); } + private Expression MatchExpressionType(Expression expression, Type typeToMatch) + => expression.Type != typeToMatch + ? Expression.Convert(expression, typeToMatch) + : expression; + private bool TryExtractEqualityOperands( Expression expression, [NotNullWhen(true)] out Expression? left, diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index 78b7ac4dad7..a6c505d7041 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -2216,6 +2216,90 @@ FROM root c WHERE ((c[""Discriminator""] = ""Customer"") AND (c[""CustomerID""] = ""ALFKI""))"); } + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task FirstOrDefault_over_scalar_projection_compared_to_null(bool async) + { + return base.FirstOrDefault_over_scalar_projection_compared_to_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task FirstOrDefault_over_scalar_projection_compared_to_not_null(bool async) + { + return base.FirstOrDefault_over_scalar_projection_compared_to_not_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task FirstOrDefault_over_custom_projection_compared_to_null(bool async) + { + return base.FirstOrDefault_over_custom_projection_compared_to_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task FirstOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + return base.FirstOrDefault_over_custom_projection_compared_to_not_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task SingleOrDefault_over_custom_projection_compared_to_null(bool async) + { + return base.SingleOrDefault_over_custom_projection_compared_to_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task SingleOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + return base.SingleOrDefault_over_custom_projection_compared_to_not_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task LastOrDefault_over_custom_projection_compared_to_null(bool async) + { + return base.LastOrDefault_over_custom_projection_compared_to_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task LastOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + return base.LastOrDefault_over_custom_projection_compared_to_not_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task First_over_custom_projection_compared_to_null(bool async) + { + return base.First_over_custom_projection_compared_to_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task First_over_custom_projection_compared_to_not_null(bool async) + { + return base.First_over_custom_projection_compared_to_not_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task Single_over_custom_projection_compared_to_null(bool async) + { + return base.Single_over_custom_projection_compared_to_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task Single_over_custom_projection_compared_to_not_null(bool async) + { + return base.Single_over_custom_projection_compared_to_not_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task Last_over_custom_projection_compared_to_null(bool async) + { + return base.Last_over_custom_projection_compared_to_null(async); + } + + [ConditionalTheory(Skip = "Issue#17246 (Cross-collection join is not supported)")] + public override Task Last_over_custom_projection_compared_to_not_null(bool async) + { + return base.Last_over_custom_projection_compared_to_not_null(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index 808cb476dc5..6ded26eb0f5 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -1204,7 +1204,7 @@ public virtual Task Where_select_many_and(bool async) ss => from c in ss.Set() from e in ss.Set() - // ReSharper disable ArrangeRedundantParentheses + // ReSharper disable ArrangeRedundantParentheses #pragma warning disable RCS1032 // Remove redundant parentheses. where (c.City == "London" && c.Country == "UK") && (e.City == "London" && e.Country == "UK") @@ -2522,6 +2522,154 @@ public virtual Task Filter_with_EF_Property_using_function_for_property_name(boo entryCount: 1); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task FirstOrDefault_over_scalar_projection_compared_to_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => (int?)o.OrderID).FirstOrDefault() == null), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task FirstOrDefault_over_scalar_projection_compared_to_not_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => (int?)o.OrderID).FirstOrDefault() != null), + entryCount: 89); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task FirstOrDefault_over_custom_projection_compared_to_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() == null), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task FirstOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() != null), + entryCount: 89); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SingleOrDefault_over_custom_projection_compared_to_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).SingleOrDefault() == null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() == null), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SingleOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).SingleOrDefault() != null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() != null), + entryCount: 89); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task LastOrDefault_over_custom_projection_compared_to_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).LastOrDefault() == null), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task LastOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).LastOrDefault() != null), + entryCount: 89); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task First_over_custom_projection_compared_to_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).First() == null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() == null), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task First_over_custom_projection_compared_to_not_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).First() != null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() != null), + entryCount: 89); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Single_over_custom_projection_compared_to_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).Single() == null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() == null), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Single_over_custom_projection_compared_to_not_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).Single() != null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).FirstOrDefault() != null), + entryCount: 89); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Last_over_custom_projection_compared_to_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).Last() == null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).LastOrDefault() == null), + entryCount: 2); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Last_over_custom_projection_compared_to_not_null(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).Last() != null), + ss => ss.Set().Where(c => c.Orders.Select(o => new { o.OrderID }).LastOrDefault() != null), + entryCount: 89); + } + private string StringMethod(string arg) => arg; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index d0565e6ac2e..e4297e065c4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -3212,11 +3212,10 @@ public override async Task Member_pushdown_with_multiple_collections(bool async) @"SELECT ( SELECT TOP(1) [l0].[Name] FROM [LevelThree] AS [l0] - WHERE ( - SELECT TOP(1) [l1].[Id] + WHERE EXISTS ( + SELECT 1 FROM [LevelTwo] AS [l1] - WHERE [l].[Id] = [l1].[OneToMany_Optional_Inverse2Id] - ORDER BY [l1].[Id]) IS NOT NULL AND ((( + WHERE [l].[Id] = [l1].[OneToMany_Optional_Inverse2Id]) AND ((( SELECT TOP(1) [l2].[Id] FROM [LevelTwo] AS [l2] WHERE [l].[Id] = [l2].[OneToMany_Optional_Inverse2Id] @@ -3701,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 ( - SELECT TOP(1) [l1].[Id] + WHERE EXISTS ( + SELECT 1 FROM [LevelTwo] AS [l1] - WHERE ([l].[Id] = [l1].[OneToMany_Optional_Inverse2Id]) AND ([l1].[Name] = N'L2 02')) IS NOT NULL AND ((( + WHERE ([l].[Id] = [l1].[OneToMany_Optional_Inverse2Id]) AND ([l1].[Name] = N'L2 02')) 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 ebe281dc370..4bc495cf9e4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -5738,11 +5738,10 @@ public override async Task Filter_with_complex_predicate_containing_subquery(boo 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 ([g].[FullName] <> N'Dom') AND ( - SELECT TOP(1) [w].[Id] +WHERE ([g].[FullName] <> N'Dom') AND EXISTS ( + SELECT 1 FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = CAST(1 AS bit)) - ORDER BY [w].[Id]) IS NOT NULL"); + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = CAST(1 AS bit)))"); } public override async Task Query_with_complex_let_containing_ordering_and_filter_projecting_firstOrDefault_element_of_let( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index 61492642b27..474dc349c7f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -591,11 +591,10 @@ 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 ( - SELECT TOP(1) [o0].[OrderID] + WHERE EXISTS ( + SELECT 1 FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID] - ORDER BY [o0].[OrderID]) IS NOT NULL AND (( + WHERE [c].[CustomerID] = [o0].[CustomerID]) AND (( SELECT TOP(1) [o1].[OrderID] FROM [Orders] AS [o1] WHERE [c].[CustomerID] = [o1].[CustomerID] @@ -614,11 +613,10 @@ public override async Task Multiple_collection_navigation_with_FirstOrDefault_ch @"SELECT ( SELECT TOP(1) [o].[ProductID] FROM [Order Details] AS [o] - WHERE ( - SELECT TOP(1) [o0].[OrderID] + WHERE EXISTS ( + SELECT 1 FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID] - ORDER BY [o0].[OrderID]) IS NOT NULL AND (( + WHERE [c].[CustomerID] = [o0].[CustomerID]) 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 322e0da4fb0..72b78dded6e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -265,10 +265,10 @@ public override async Task Entity_equality_through_subquery(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE ( - SELECT TOP(1) [o].[OrderID] +WHERE EXISTS ( + SELECT 1 FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID]) IS NOT NULL"); + WHERE [c].[CustomerID] = [o].[CustomerID])"); } public override async Task Entity_equality_through_include(bool async) @@ -450,10 +450,10 @@ SELECT TOP(@__p_0) [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], FROM [Employees] AS [e] ORDER BY [e].[EmployeeID] ) AS [t] -WHERE ( - SELECT TOP(1) [e0].[EmployeeID] +WHERE NOT (EXISTS ( + SELECT 1 FROM [Employees] AS [e0] - WHERE [e0].[EmployeeID] = [t].[ReportsTo]) IS NULL + WHERE [e0].[EmployeeID] = [t].[ReportsTo])) ORDER BY [t].[EmployeeID]"); } @@ -472,10 +472,10 @@ FROM [Employees] AS [e] ORDER BY [e].[EmployeeID] OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY ) AS [t] -WHERE ( - SELECT TOP(1) [e0].[EmployeeID] +WHERE EXISTS ( + SELECT 1 FROM [Employees] AS [e0] - WHERE [e0].[EmployeeID] = [t].[ReportsTo]) IS NOT NULL + WHERE [e0].[EmployeeID] = [t].[ReportsTo]) ORDER BY [t].[EmployeeID]"); } @@ -3889,11 +3889,10 @@ 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 ( - SELECT TOP(1) [o].[CustomerID] +WHERE NOT (EXISTS ( + SELECT 1 FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderID] DESC) IS NULL"); + WHERE [c].[CustomerID] = [o].[CustomerID]))"); } public override async Task Subquery_is_not_null_translated_correctly(bool async) @@ -3903,11 +3902,10 @@ 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 ( - SELECT TOP(1) [o].[CustomerID] +WHERE EXISTS ( + SELECT 1 FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderID] DESC) IS NOT NULL"); + WHERE [c].[CustomerID] = [o].[CustomerID])"); } public override async Task Select_take_average(bool async) @@ -4554,11 +4552,10 @@ FROM [Orders] AS [o0] WHERE [c].[CustomerID] = [o0].[CustomerID] ORDER BY [o0].[OrderDate]) AS [OrderDate] FROM [Customers] AS [c] -WHERE ([c].[CustomerID] LIKE N'A%') AND ( - SELECT TOP(1) [o].[OrderID] +WHERE ([c].[CustomerID] LIKE N'A%') AND EXISTS ( + SELECT 1 FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderDate]) IS NOT NULL"); + WHERE [c].[CustomerID] = [o].[CustomerID])"); } public override async Task Let_entity_equality_to_other_entity(bool async) @@ -4604,12 +4601,11 @@ 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 ( - SELECT TOP(1) [c0].[CustomerID] +WHERE NOT (EXISTS ( + SELECT 1 FROM [Orders] AS [o] LEFT JOIN [Customers] AS [c0] ON [o].[CustomerID] = [c0].[CustomerID] - WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderID]) IS NULL"); + WHERE [c].[CustomerID] = [o].[CustomerID]))"); } public override async Task Collection_navigation_equality_rewrite_for_subquery(bool async) @@ -5132,12 +5128,11 @@ public override async Task Pending_selector_in_cardinality_reducing_method_is_ap WHEN EXISTS ( SELECT 1 FROM [Orders] AS [o] - WHERE (( - SELECT TOP(1) [c0].[CustomerID] + WHERE (EXISTS ( + SELECT 1 FROM [Orders] AS [o0] LEFT JOIN [Customers] AS [c0] ON [o0].[CustomerID] = [c0].[CustomerID] - WHERE [c].[CustomerID] = [o0].[CustomerID] - ORDER BY [o0].[OrderDate]) IS NOT NULL AND ((( + WHERE [c].[CustomerID] = [o0].[CustomerID]) AND ((( SELECT TOP(1) [c1].[CustomerID] FROM [Orders] AS [o1] LEFT JOIN [Customers] AS [c1] ON [o1].[CustomerID] = [c1].[CustomerID] @@ -5202,10 +5197,10 @@ public override async Task Entity_equality_on_subquery_with_null_check(bool asyn WHEN NOT (EXISTS ( SELECT 1 FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID])) OR ( - SELECT TOP(1) [o0].[OrderID] + WHERE [c].[CustomerID] = [o].[CustomerID])) OR NOT (EXISTS ( + SELECT 1 FROM [Orders] AS [o0] - WHERE [c].[CustomerID] = [o0].[CustomerID]) IS NULL THEN CAST(1 AS bit) + WHERE [c].[CustomerID] = [o0].[CustomerID])) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END, ( SELECT TOP(1) [o1].[OrderDate] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index d523bf45235..b240c57bca5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -1692,11 +1692,10 @@ public override async Task Where_subquery_FirstOrDefault_is_null(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 ( - SELECT TOP(1) [o].[OrderID] +WHERE NOT (EXISTS ( + SELECT 1 FROM [Orders] AS [o] - WHERE [c].[CustomerID] = [o].[CustomerID] - ORDER BY [o].[OrderID]) IS NULL"); + WHERE [c].[CustomerID] = [o].[CustomerID]))"); } public override async Task Where_subquery_FirstOrDefault_compared_to_entity(bool async) @@ -2289,6 +2288,188 @@ FROM [Customers] AS [c] WHERE [c].[CustomerID] = N'ALFKI'"); } + public override async Task FirstOrDefault_over_scalar_projection_compared_to_null(bool async) + { + await base.FirstOrDefault_over_scalar_projection_compared_to_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]))"); + } + + public override async Task FirstOrDefault_over_scalar_projection_compared_to_not_null(bool async) + { + await base.FirstOrDefault_over_scalar_projection_compared_to_not_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])"); + } + + public override async Task FirstOrDefault_over_custom_projection_compared_to_null(bool async) + { + await base.FirstOrDefault_over_custom_projection_compared_to_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]))"); + } + + public override async Task FirstOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + await base.FirstOrDefault_over_custom_projection_compared_to_not_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])"); + } + + public override async Task SingleOrDefault_over_custom_projection_compared_to_null(bool async) + { + await base.SingleOrDefault_over_custom_projection_compared_to_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]))"); + } + + public override async Task SingleOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + await base.SingleOrDefault_over_custom_projection_compared_to_not_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])"); + } + + public override async Task LastOrDefault_over_custom_projection_compared_to_null(bool async) + { + await base.LastOrDefault_over_custom_projection_compared_to_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]))"); + } + + public override async Task LastOrDefault_over_custom_projection_compared_to_not_null(bool async) + { + await base.LastOrDefault_over_custom_projection_compared_to_not_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])"); + } + + public override async Task First_over_custom_projection_compared_to_null(bool async) + { + await base.First_over_custom_projection_compared_to_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]))"); + } + + public override async Task First_over_custom_projection_compared_to_not_null(bool async) + { + await base.First_over_custom_projection_compared_to_not_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])"); + } + + public override async Task Single_over_custom_projection_compared_to_null(bool async) + { + await base.Single_over_custom_projection_compared_to_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]))"); + } + + public override async Task Single_over_custom_projection_compared_to_not_null(bool async) + { + await base.Single_over_custom_projection_compared_to_not_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])"); + } + + public override async Task Last_over_custom_projection_compared_to_null(bool async) + { + await base.Last_over_custom_projection_compared_to_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]))"); + } + + public override async Task Last_over_custom_projection_compared_to_not_null(bool async) + { + await base.Last_over_custom_projection_compared_to_not_null(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 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID])"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 549600a8076..f5fb891b70c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -4061,8 +4061,8 @@ public virtual async Task Projecting_column_with_value_converter_of_ulong_byte_a { var result = context.Parents.OrderBy(e => e.Id).Select(p => (ulong?)p.Child.ULongRowVersion).FirstOrDefault(); - AssertSql( - @"SELECT TOP(1) [c].[ULongRowVersion] + AssertSql( + @"SELECT TOP(1) [c].[ULongRowVersion] FROM [Parents] AS [p] LEFT JOIN [Children] AS [c] ON [p].[ChildId] = [c].[Id] ORDER BY [p].[Id]"); @@ -5338,10 +5338,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 ( - SELECT TOP(1) [v].[Id] + WHERE EXISTS ( + SELECT 1 FROM [Values] AS [v] - WHERE [e].[Id] = [v].[Entity11023Id]) IS NOT NULL AND ((( + WHERE [e].[Id] = [v].[Entity11023Id]) AND ((( SELECT TOP(1) [v0].[Id] FROM [Values] AS [v0] WHERE [e].[Id] = [v0].[Entity11023Id]) = [t].[Value11023Id]) OR (( @@ -7089,10 +7089,10 @@ public virtual async Task Using_explicit_interface_implementation_as_navigation_ AssertSql( @"SELECT TOP(2) CASE - WHEN ( - SELECT TOP(1) [c].[Id] + WHEN EXISTS ( + SELECT 1 FROM [CoverIllustrations] AS [c] - WHERE ([b0].[Id] = [c].[CoverId]) AND ([c].[State] >= 2)) IS NOT NULL THEN CAST(1 AS bit) + WHERE ([b0].[Id] = [c].[CoverId]) AND ([c].[State] >= 2)) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END, ( SELECT TOP(1) [c0].[Uri] @@ -7228,8 +7228,8 @@ from t2 in context.Tests.FromSqlInterpolated($"Select * from Tests Where Type = Assert.Equal(MyContext19206.TestType19206.Unit, item.t1.Type); Assert.Equal(MyContext19206.TestType19206.Integration, item.t2.Type); - AssertSql( - @"p0='0' + AssertSql( + @"p0='0' p1='1' SELECT [m].[Id], [m].[Type], [m0].[Id], [m0].[Type] @@ -10120,8 +10120,8 @@ public virtual async Task NoTracking_split_query_creates_only_required_instances Assert.Equal(1, Test25400.ConstructorCallCount); - AssertSql( - @"SELECT TOP(1) [t].[Id], [t].[Value] + AssertSql( + @"SELECT TOP(1) [t].[Id], [t].[Value] FROM [Tests] AS [t] ORDER BY [t].[Id]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 954d7e7f02b..7b98acf1e21 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -6704,11 +6704,10 @@ 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 ([g].[FullName] <> N'Dom') AND ( - SELECT TOP(1) [w].[Id] +WHERE ([g].[FullName] <> N'Dom') AND EXISTS ( + SELECT 1 FROM [Weapons] AS [w] - WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = CAST(1 AS bit)) - ORDER BY [w].[Id]) IS NOT NULL"); + WHERE ([g].[FullName] = [w].[OwnerFullName]) AND ([w].[IsAutomatic] = CAST(1 AS bit)))"); } public override async Task Query_with_complex_let_containing_ordering_and_filter_projecting_firstOrDefault_element_of_let(