diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs index 94c2cdbfc1f..b86d1f69e1f 100644 --- a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs @@ -484,6 +484,14 @@ private static Expression UnfoldStructuralComparison(ExpressionType expressionTy && rightExpressions != null && leftExpressions.Length == rightExpressions.Length) { + if (leftExpressions.Length == 1 + && expressionType == ExpressionType.Equal) + { + var translatedExpression = TransformNullComparison(leftExpressions[0], rightExpressions[0], binaryExpression.NodeType) + ?? Expression.MakeBinary(expressionType, leftExpressions[0], rightExpressions[0]); + return Expression.AndAlso(translatedExpression, Expression.Constant(true, translatedExpression.Type)); + } + return leftExpressions .Zip(rightExpressions, (l, r) => TransformNullComparison(l, r, binaryExpression.NodeType) diff --git a/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs b/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs index 80e95804519..c4eb953356e 100644 --- a/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/Sql/DefaultQuerySqlGenerator.cs @@ -29,6 +29,11 @@ public class DefaultQuerySqlGenerator : ThrowingExpressionVisitor, ISqlExpressio private IReadOnlyDictionary _parametersValues; private ParameterNameGenerator _parameterNameGenerator; private RelationalTypeMapping _typeMapping; + private RelationalNullsExpandingVisitor _relationalNullsExpandingVisitor; + private PredicateReductionExpressionOptimizer _predicateReductionExpressionOptimizer; + private PredicateNegationExpressionOptimizer _predicateNegationExpressionOptimizer; + private ReducingExpressionVisitor _reducingExpressionVisitor; + private BooleanExpressionTranslatingVisitor _booleanExpressionTranslatingVisitor; private static readonly Dictionary _operatorMap = new Dictionary { @@ -266,48 +271,63 @@ var newExpression = new NullComparisonTransformingVisitor(_parametersValues) .Visit(expression); - var binaryExpression = newExpression as BinaryExpression; - var relationalNullsOptimizedExpandingVisitor = new RelationalNullsOptimizedExpandingVisitor(); - var relationalNullsExpandingVisitor = new RelationalNullsExpandingVisitor(); + if (_relationalNullsExpandingVisitor == null) + { + _relationalNullsExpandingVisitor = new RelationalNullsExpandingVisitor(); + } - if (joinCondition - && binaryExpression != null) + if (_predicateReductionExpressionOptimizer == null) { - var optimizedLeftExpression = relationalNullsOptimizedExpandingVisitor.Visit(binaryExpression.Left); + _predicateReductionExpressionOptimizer = new PredicateReductionExpressionOptimizer(); + } - optimizedLeftExpression - = relationalNullsOptimizedExpandingVisitor.IsOptimalExpansion - ? optimizedLeftExpression - : relationalNullsExpandingVisitor.Visit(binaryExpression.Left); + if (_predicateNegationExpressionOptimizer == null) + { + _predicateNegationExpressionOptimizer = new PredicateNegationExpressionOptimizer(); + } - relationalNullsOptimizedExpandingVisitor = new RelationalNullsOptimizedExpandingVisitor(); - var optimizedRightExpression = relationalNullsOptimizedExpandingVisitor.Visit(binaryExpression.Right); + if (_reducingExpressionVisitor == null) + { + _reducingExpressionVisitor = new ReducingExpressionVisitor(); + } - optimizedRightExpression - = relationalNullsOptimizedExpandingVisitor.IsOptimalExpansion - ? optimizedRightExpression - : relationalNullsExpandingVisitor.Visit(binaryExpression.Right); + if (_booleanExpressionTranslatingVisitor == null) + { + _booleanExpressionTranslatingVisitor = new BooleanExpressionTranslatingVisitor(); + } - newExpression = Expression.MakeBinary(binaryExpression.NodeType, optimizedLeftExpression, optimizedRightExpression); + if (joinCondition + && newExpression is BinaryExpression binaryExpression + && binaryExpression.NodeType == ExpressionType.Equal) + { + newExpression = Expression.MakeBinary( + binaryExpression.NodeType, + ApplyNullSemantics(binaryExpression.Left), + ApplyNullSemantics(binaryExpression.Right)); } else { - var optimizedExpression = relationalNullsOptimizedExpandingVisitor.Visit(newExpression); - - newExpression - = relationalNullsOptimizedExpandingVisitor.IsOptimalExpansion - ? optimizedExpression - : relationalNullsExpandingVisitor.Visit(newExpression); + newExpression = ApplyNullSemantics(newExpression); } - newExpression = new PredicateReductionExpressionOptimizer().Visit(newExpression); - newExpression = new PredicateNegationExpressionOptimizer().Visit(newExpression); - newExpression = new ReducingExpressionVisitor().Visit(newExpression); - newExpression = new BooleanExpressionTranslatingVisitor().Translate(newExpression, searchCondition: searchCondition); + newExpression = _predicateReductionExpressionOptimizer.Visit(newExpression); + newExpression = _predicateNegationExpressionOptimizer.Visit(newExpression); + newExpression = _reducingExpressionVisitor.Visit(newExpression); + newExpression = _booleanExpressionTranslatingVisitor.Translate(newExpression, searchCondition: searchCondition); return newExpression; } + private Expression ApplyNullSemantics(Expression expression) + { + var relationalNullsOptimizedExpandingVisitor = new RelationalNullsOptimizedExpandingVisitor(); + var optimizedRightExpression = relationalNullsOptimizedExpandingVisitor.Visit(expression); + + return relationalNullsOptimizedExpandingVisitor.IsOptimalExpansion + ? optimizedRightExpression + : _relationalNullsExpandingVisitor.Visit(expression); + } + /// /// Visit a single projection in SQL SELECT clause /// diff --git a/src/EFCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs b/src/EFCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs index 9cd664c06ec..30641da4575 100644 --- a/src/EFCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs +++ b/src/EFCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs @@ -3143,7 +3143,7 @@ from l1 in grouping.DefaultIfEmpty() select new { Id = l2.Id, Nane = l1 != null ? l1.Name : null }, (l1s, l2s) => from l2 in l2s - join l1 in l1s.OrderBy(x => Maybe(x.OneToOne_Optional_FK, () => x.OneToOne_Optional_FK.Name)).Take(2) + join l1 in l1s.OrderBy(x => Maybe(x.OneToOne_Optional_FK, () => x.OneToOne_Optional_FK.Name)).Take(2) on l2.Level1_Optional_Id equals l1.Id into grouping from l1 in grouping.DefaultIfEmpty() select new { Id = l2.Id, Nane = l1 != null ? l1.Name : null }, @@ -3792,6 +3792,56 @@ public virtual void Where_on_multilevel_reference_in_subquery_with_outer_project .Select(l3 => l3.Name)); } + [ConditionalFact] + public virtual void Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property() + { + using (var context = CreateContext()) + { + var query = from l1 in context.LevelOne + join l2 in context.LevelTwo + on new + { + A = EF.Property(l1, "OneToMany_Optional_Self_InverseId"), + } + equals new + { + A = EF.Property(l2, "Level1_Optional_Id"), + } + select l1; + + var result = query.ToList(); + + // This result is manually verified. Do not change. + Assert.Equal(53, result.Count); + } + } + + [ConditionalFact] + public virtual void Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties() + { + using (var context = CreateContext()) + { + var query = from l1 in context.LevelOne + join l2 in context.LevelTwo + on new + { + A = EF.Property(l1, "OneToMany_Optional_Self_InverseId"), + B = EF.Property(l1, "OneToOne_Optional_SelfId") + } + equals new + { + A = EF.Property(l2, "Level1_Optional_Id"), + B = EF.Property(l2, "OneToMany_Optional_Self_InverseId"), + } + select l1; + + var result = query.ToList(); + + // This result is manually verified. Do not change. + Assert.Equal(39, result.Count); + } + } + private static TResult Maybe(object caller, Func expression) where TResult : class { if (caller == null) diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs index 710694be423..2ec742205af 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs @@ -2659,6 +2659,28 @@ OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY Sql); } + public override void Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property() + { + base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_single_property(); + + Assert.Equal( + @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_InverseId], [l1].[OneToMany_Required_Self_InverseId], [l1].[OneToOne_Optional_SelfId] +FROM [Level1] AS [l1] +INNER JOIN [Level2] AS [l2] ON ([l1].[OneToMany_Optional_Self_InverseId] = [l2].[Level1_Optional_Id]) OR ([l1].[OneToMany_Optional_Self_InverseId] IS NULL AND [l2].[Level1_Optional_Id] IS NULL)", + Sql); + } + + public override void Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties() + { + base.Join_condition_optimizations_applied_correctly_when_anonymous_type_with_multiple_properties(); + + Assert.Equal( + @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_InverseId], [l1].[OneToMany_Required_Self_InverseId], [l1].[OneToOne_Optional_SelfId] +FROM [Level1] AS [l1] +INNER JOIN [Level2] AS [l2] ON (([l1].[OneToMany_Optional_Self_InverseId] = [l2].[Level1_Optional_Id]) OR ([l1].[OneToMany_Optional_Self_InverseId] IS NULL AND [l2].[Level1_Optional_Id] IS NULL)) AND (([l1].[OneToOne_Optional_SelfId] = [l2].[OneToMany_Optional_Self_InverseId]) OR ([l1].[OneToOne_Optional_SelfId] IS NULL AND [l2].[OneToMany_Optional_Self_InverseId] IS NULL))", + Sql); + } + private const string FileLineEnding = @" ";