From a938a3444d1d48cccae7b6de6c3c9908b152d51d Mon Sep 17 00:00:00 2001 From: Maurycy Markowski Date: Mon, 2 Dec 2019 12:30:32 -0800 Subject: [PATCH] Additional refactoring of Null Semantics: - moving NullSemantics visitor after 2nd level cache - we need to know the parameter values to properly handle IN expressions wrt null semantics, - NullSemantics visitor needs to go before SqlExpressionOptimizer and SearchCondition, so those two are also moved after 2nd level cache, - combining NullSemantics with SqlExpressionOptimizer (kept SqlExpressionOptimizer name) - SearchCondition now applies some relevant optimization itself, so that we don't need to run full optimizer afterwards, - merging InExpressionValuesExpandingExpressionVisitor into SqlExpressionOptimizer as well, so that we don't apply the rewrite for UseRelationalNulls, - preventing NulSemantics from performing double visitation when computing non-nullable columns. Resolves #11464 Resolves #15722 Resolves #18338 Resolves #18597 Resolves #18689 Resolves #19019 --- ...omSqlParameterApplyingExpressionVisitor.cs | 134 ++ ...NullSemanticsRewritingExpressionVisitor.cs | 941 --------- ...qlExpressionOptimizingExpressionVisitor.cs | 510 ----- ...meterBasedQueryTranslationPostprocessor.cs | 304 +-- ...RelationalQueryTranslationPostprocessor.cs | 16 +- ...qlExpressionOptimizingExpressionVisitor.cs | 1723 +++++++++++++++++ .../Query/SqlExpressions/InExpression.cs | 4 +- ...rchConditionConvertingExpressionVisitor.cs | 44 +- ...meterBasedQueryTranslationPostprocessor.cs | 4 +- .../SqlServerQueryTranslationPostprocessor.cs | 9 - .../Query/NorthwindJoinQueryCosmosTest.cs | 12 + .../Query/NullSemanticsQueryTestBase.cs | 193 +- .../Query/ComplexNavigationsQueryTestBase.cs | 2 +- .../Query/GearsOfWarQueryTestBase.cs | 14 +- .../Query/NorthwindJoinQueryTestBase.cs | 23 + .../LoadSqlServerTest.cs | 16 +- .../ComplexNavigationsQuerySqlServerTest.cs | 9 +- .../Query/FunkyDataQuerySqlServerTest.cs | 44 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 65 +- .../Query/InheritanceSqlServerTest.cs | 2 +- ...indAggregateOperatorsQuerySqlServerTest.cs | 10 +- .../NorthwindFunctionsQuerySqlServerTest.cs | 12 +- .../NorthwindGroupByQuerySqlServerTest.cs | 4 +- .../Query/NorthwindJoinQuerySqlServerTest.cs | 46 +- ...orthwindMiscellaneousQuerySqlServerTest.cs | 18 +- .../NorthwindSelectQuerySqlServerTest.cs | 2 +- .../Query/NorthwindWhereQuerySqlServerTest.cs | 28 +- .../Query/NullSemanticsQuerySqlServerTest.cs | 249 ++- .../Query/OwnedQuerySqlServerTest.cs | 2 +- .../QueryFilterFuncletizationSqlServerTest.cs | 2 +- .../NorthwindFunctionsQuerySqliteTest.cs | 8 +- 31 files changed, 2513 insertions(+), 1937 deletions(-) create mode 100644 src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs delete mode 100644 src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs delete mode 100644 src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressionOptimizingExpressionVisitor.cs diff --git a/src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs new file mode 100644 index 00000000000..17b67401cdd --- /dev/null +++ b/src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class FromSqlParameterApplyingExpressionVisitor : ExpressionVisitor + { + private readonly IDictionary _visitedFromSqlExpressions + = new Dictionary(ReferenceEqualityComparer.Instance); + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly ParameterNameGenerator _parameterNameGenerator; + private readonly IReadOnlyDictionary _parametersValues; + + public FromSqlParameterApplyingExpressionVisitor( + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + [NotNull] ParameterNameGenerator parameterNameGenerator, + [NotNull] IReadOnlyDictionary parametersValues) + { + Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); + Check.NotNull(parameterNameGenerator, nameof(parameterNameGenerator)); + Check.NotNull(parametersValues, nameof(parametersValues)); + + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGenerator = parameterNameGenerator; + _parametersValues = parametersValues; + } + + public override Expression Visit(Expression expression) + { + if (expression is FromSqlExpression fromSql) + { + if (!_visitedFromSqlExpressions.TryGetValue(fromSql, out var updatedFromSql)) + { + switch (fromSql.Arguments) + { + case ParameterExpression parameterExpression: + var parameterValues = (object[])_parametersValues[parameterExpression.Name]; + + var subParameters = new List(parameterValues.Length); + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < parameterValues.Length; i++) + { + var parameterName = _parameterNameGenerator.GenerateNext(); + if (parameterValues[i] is DbParameter dbParameter) + { + if (string.IsNullOrEmpty(dbParameter.ParameterName)) + { + dbParameter.ParameterName = parameterName; + } + else + { + parameterName = dbParameter.ParameterName; + } + + subParameters.Add(new RawRelationalParameter(parameterName, dbParameter)); + } + else + { + subParameters.Add( + new TypeMappedRelationalParameter( + parameterName, + parameterName, + _sqlExpressionFactory.GetTypeMappingForValue(parameterValues[i]), + parameterValues[i]?.GetType().IsNullableType())); + } + } + + updatedFromSql = new FromSqlExpression( + fromSql.Sql, + Expression.Constant( + new CompositeRelationalParameter( + parameterExpression.Name, + subParameters)), + fromSql.Alias); + + _visitedFromSqlExpressions[fromSql] = updatedFromSql; + break; + + case ConstantExpression constantExpression: + var existingValues = (object[])constantExpression.Value; + var constantValues = new object[existingValues.Length]; + for (var i = 0; i < existingValues.Length; i++) + { + var value = existingValues[i]; + if (value is DbParameter dbParameter) + { + var parameterName = _parameterNameGenerator.GenerateNext(); + if (string.IsNullOrEmpty(dbParameter.ParameterName)) + { + dbParameter.ParameterName = parameterName; + } + else + { + parameterName = dbParameter.ParameterName; + } + + constantValues[i] = new RawRelationalParameter(parameterName, dbParameter); + } + else + { + constantValues[i] = _sqlExpressionFactory.Constant( + value, _sqlExpressionFactory.GetTypeMappingForValue(value)); + } + } + + updatedFromSql = new FromSqlExpression( + fromSql.Sql, + Expression.Constant(constantValues, typeof(object[])), + fromSql.Alias); + + _visitedFromSqlExpressions[fromSql] = updatedFromSql; + break; + } + } + + return updatedFromSql; + } + + return base.Visit(expression); + } + } +} diff --git a/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs deleted file mode 100644 index bd97aead1a3..00000000000 --- a/src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs +++ /dev/null @@ -1,941 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Utilities; - -namespace Microsoft.EntityFrameworkCore.Query.Internal -{ - public class NullSemanticsRewritingExpressionVisitor : SqlExpressionVisitor - { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - - private bool _isNullable; - private bool _canOptimize; - private readonly List _nonNullableColumns = new List(); - - public NullSemanticsRewritingExpressionVisitor([NotNull] ISqlExpressionFactory sqlExpressionFactory) - { - _sqlExpressionFactory = sqlExpressionFactory; - _canOptimize = true; - } - - protected override Expression VisitCase(CaseExpression caseExpression) - { - Check.NotNull(caseExpression, nameof(caseExpression)); - - _isNullable = false; - // if there is no 'else' there is a possibility of null, when none of the conditions are met - // otherwise the result is nullable if any of the WhenClause results OR ElseResult is nullable - var isNullable = caseExpression.ElseResult == null; - - var canOptimize = _canOptimize; - var testIsCondition = caseExpression.Operand == null; - _canOptimize = false; - var newOperand = (SqlExpression)Visit(caseExpression.Operand); - var newWhenClauses = new List(); - foreach (var whenClause in caseExpression.WhenClauses) - { - _canOptimize = testIsCondition; - var newTest = (SqlExpression)Visit(whenClause.Test); - _canOptimize = false; - _isNullable = false; - var newResult = (SqlExpression)Visit(whenClause.Result); - isNullable |= _isNullable; - newWhenClauses.Add(new CaseWhenClause(newTest, newResult)); - } - - _canOptimize = false; - var newElseResult = (SqlExpression)Visit(caseExpression.ElseResult); - _isNullable |= isNullable; - _canOptimize = canOptimize; - - return caseExpression.Update(newOperand, newWhenClauses, newElseResult); - } - - protected override Expression VisitColumn(ColumnExpression columnExpression) - { - Check.NotNull(columnExpression, nameof(columnExpression)); - - _isNullable = !_nonNullableColumns.Contains(columnExpression) && columnExpression.IsNullable; - - return columnExpression; - } - - protected override Expression VisitCrossApply(CrossApplyExpression crossApplyExpression) - { - Check.NotNull(crossApplyExpression, nameof(crossApplyExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var table = (TableExpressionBase)Visit(crossApplyExpression.Table); - _canOptimize = canOptimize; - - return crossApplyExpression.Update(table); - } - - protected override Expression VisitCrossJoin(CrossJoinExpression crossJoinExpression) - { - Check.NotNull(crossJoinExpression, nameof(crossJoinExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var table = (TableExpressionBase)Visit(crossJoinExpression.Table); - _canOptimize = canOptimize; - - return crossJoinExpression.Update(table); - } - - protected override Expression VisitExcept(ExceptExpression exceptExpression) - { - Check.NotNull(exceptExpression, nameof(exceptExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var source1 = (SelectExpression)Visit(exceptExpression.Source1); - var source2 = (SelectExpression)Visit(exceptExpression.Source2); - _canOptimize = canOptimize; - - return exceptExpression.Update(source1, source2); - } - - protected override Expression VisitExists(ExistsExpression existsExpression) - { - Check.NotNull(existsExpression, nameof(existsExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var newSubquery = (SelectExpression)Visit(existsExpression.Subquery); - _canOptimize = canOptimize; - - return existsExpression.Update(newSubquery); - } - - protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) - { - Check.NotNull(fromSqlExpression, nameof(fromSqlExpression)); - - return fromSqlExpression; - } - - protected override Expression VisitIn(InExpression inExpression) - { - Check.NotNull(inExpression, nameof(inExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - _isNullable = false; - var item = (SqlExpression)Visit(inExpression.Item); - var isNullable = _isNullable; - _isNullable = false; - var subquery = (SelectExpression)Visit(inExpression.Subquery); - isNullable |= _isNullable; - _isNullable = false; - var values = (SqlExpression)Visit(inExpression.Values); - _isNullable |= isNullable; - _canOptimize = canOptimize; - - return inExpression.Update(item, values, subquery); - } - - protected override Expression VisitIntersect(IntersectExpression intersectExpression) - { - Check.NotNull(intersectExpression, nameof(intersectExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var source1 = (SelectExpression)Visit(intersectExpression.Source1); - var source2 = (SelectExpression)Visit(intersectExpression.Source2); - _canOptimize = canOptimize; - - return intersectExpression.Update(source1, source2); - } - - protected override Expression VisitLike(LikeExpression likeExpression) - { - Check.NotNull(likeExpression, nameof(likeExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - _isNullable = false; - var newMatch = (SqlExpression)Visit(likeExpression.Match); - var isNullable = _isNullable; - _isNullable = false; - var newPattern = (SqlExpression)Visit(likeExpression.Pattern); - isNullable |= _isNullable; - _isNullable = false; - var newEscapeChar = (SqlExpression)Visit(likeExpression.EscapeChar); - _isNullable |= isNullable; - _canOptimize = canOptimize; - - return likeExpression.Update(newMatch, newPattern, newEscapeChar); - } - - protected override Expression VisitInnerJoin(InnerJoinExpression innerJoinExpression) - { - Check.NotNull(innerJoinExpression, nameof(innerJoinExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var newTable = (TableExpressionBase)Visit(innerJoinExpression.Table); - var newJoinPredicate = VisitJoinPredicate((SqlBinaryExpression)innerJoinExpression.JoinPredicate); - _canOptimize = canOptimize; - - return innerJoinExpression.Update(newTable, newJoinPredicate); - } - - protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpression) - { - Check.NotNull(leftJoinExpression, nameof(leftJoinExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var newTable = (TableExpressionBase)Visit(leftJoinExpression.Table); - var newJoinPredicate = VisitJoinPredicate((SqlBinaryExpression)leftJoinExpression.JoinPredicate); - _canOptimize = canOptimize; - - return leftJoinExpression.Update(newTable, newJoinPredicate); - } - - private SqlExpression VisitJoinPredicate(SqlBinaryExpression predicate) - { - var canOptimize = _canOptimize; - _canOptimize = true; - - if (predicate.OperatorType == ExpressionType.Equal) - { - var newLeft = (SqlExpression)Visit(predicate.Left); - var newRight = (SqlExpression)Visit(predicate.Right); - _canOptimize = canOptimize; - - return predicate.Update(newLeft, newRight); - } - - if (predicate.OperatorType == ExpressionType.AndAlso) - { - var newPredicate = (SqlExpression)VisitSqlBinary(predicate); - _canOptimize = canOptimize; - - return newPredicate; - } - - throw new InvalidOperationException("Unexpected join predicate shape: " + predicate); - } - - protected override Expression VisitOrdering(OrderingExpression orderingExpression) - { - Check.NotNull(orderingExpression, nameof(orderingExpression)); - - var expression = (SqlExpression)Visit(orderingExpression.Expression); - - return orderingExpression.Update(expression); - } - - protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExpression) - { - Check.NotNull(outerApplyExpression, nameof(outerApplyExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var table = (TableExpressionBase)Visit(outerApplyExpression.Table); - _canOptimize = canOptimize; - - return outerApplyExpression.Update(table); - } - - protected override Expression VisitProjection(ProjectionExpression projectionExpression) - { - Check.NotNull(projectionExpression, nameof(projectionExpression)); - - var expression = (SqlExpression)Visit(projectionExpression.Expression); - - return projectionExpression.Update(expression); - } - - protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression) - { - Check.NotNull(rowNumberExpression, nameof(rowNumberExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var changed = false; - var partitions = new List(); - foreach (var partition in rowNumberExpression.Partitions) - { - var newPartition = (SqlExpression)Visit(partition); - changed |= newPartition != partition; - partitions.Add(newPartition); - } - - var orderings = new List(); - foreach (var ordering in rowNumberExpression.Orderings) - { - var newOrdering = (OrderingExpression)Visit(ordering); - changed |= newOrdering != ordering; - orderings.Add(newOrdering); - } - - _canOptimize = canOptimize; - - return rowNumberExpression.Update(partitions, orderings); - } - - protected override Expression VisitScalarSubquery(ScalarSubqueryExpression scalarSubqueryExpression) - { - Check.NotNull(scalarSubqueryExpression, nameof(scalarSubqueryExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var subquery = (SelectExpression)Visit(scalarSubqueryExpression.Subquery); - _canOptimize = canOptimize; - - return scalarSubqueryExpression.Update(subquery); - } - - protected override Expression VisitSelect(SelectExpression selectExpression) - { - Check.NotNull(selectExpression, nameof(selectExpression)); - - var changed = false; - var canOptimize = _canOptimize; - var projections = new List(); - _canOptimize = false; - foreach (var item in selectExpression.Projection) - { - var updatedProjection = (ProjectionExpression)Visit(item); - projections.Add(updatedProjection); - changed |= updatedProjection != item; - } - - var tables = new List(); - foreach (var table in selectExpression.Tables) - { - var newTable = (TableExpressionBase)Visit(table); - changed |= newTable != table; - tables.Add(newTable); - } - - _canOptimize = true; - var predicate = (SqlExpression)Visit(selectExpression.Predicate); - changed |= predicate != selectExpression.Predicate; - - var groupBy = new List(); - _canOptimize = false; - foreach (var groupingKey in selectExpression.GroupBy) - { - var newGroupingKey = (SqlExpression)Visit(groupingKey); - changed |= newGroupingKey != groupingKey; - groupBy.Add(newGroupingKey); - } - - _canOptimize = true; - var havingExpression = (SqlExpression)Visit(selectExpression.Having); - changed |= havingExpression != selectExpression.Having; - - var orderings = new List(); - _canOptimize = false; - foreach (var ordering in selectExpression.Orderings) - { - var orderingExpression = (SqlExpression)Visit(ordering.Expression); - changed |= orderingExpression != ordering.Expression; - orderings.Add(ordering.Update(orderingExpression)); - } - - var offset = (SqlExpression)Visit(selectExpression.Offset); - changed |= offset != selectExpression.Offset; - - var limit = (SqlExpression)Visit(selectExpression.Limit); - changed |= limit != selectExpression.Limit; - - _canOptimize = canOptimize; - - // we assume SelectExpression can always be null - // (e.g. projecting non-nullable column but with predicate that filters out all rows) - _isNullable = true; - - return changed - ? selectExpression.Update( - projections, tables, predicate, groupBy, havingExpression, orderings, limit, offset, selectExpression.IsDistinct, - selectExpression.Alias) - : selectExpression; - } - - protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression) - { - Check.NotNull(sqlBinaryExpression, nameof(sqlBinaryExpression)); - - _isNullable = false; - var canOptimize = _canOptimize; - - // for SqlServer we could also allow optimize on children of ExpressionType.Equal - // because they get converted to CASE blocks anyway, but for other providers it's incorrect - // once/if null semantics optimizations are provider-specific we can enable it - _canOptimize = _canOptimize && (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso - || sqlBinaryExpression.OperatorType == ExpressionType.OrElse); - - var nonNullableColumns = new List(); - if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso) - { - nonNullableColumns = FindNonNullableColumns(sqlBinaryExpression.Left); - } - - var newLeft = (SqlExpression)Visit(sqlBinaryExpression.Left); - var leftNullable = _isNullable; - - _isNullable = false; - if (nonNullableColumns.Count > 0) - { - _nonNullableColumns.AddRange(nonNullableColumns); - } - - var newRight = (SqlExpression)Visit(sqlBinaryExpression.Right); - var rightNullable = _isNullable; - - foreach (var nonNullableColumn in nonNullableColumns) - { - _nonNullableColumns.Remove(nonNullableColumn); - } - - if (sqlBinaryExpression.OperatorType == ExpressionType.Coalesce) - { - _isNullable = leftNullable && rightNullable; - _canOptimize = canOptimize; - - return sqlBinaryExpression.Update(newLeft, newRight); - } - - if (sqlBinaryExpression.OperatorType == ExpressionType.Add - && sqlBinaryExpression.Type == typeof(string)) - { - if (leftNullable) - { - newLeft = newLeft is SqlConstantExpression - ? _sqlExpressionFactory.Constant(string.Empty) - : newLeft is ColumnExpression || newLeft is SqlParameterExpression - ? _sqlExpressionFactory.Coalesce(newLeft, _sqlExpressionFactory.Constant(string.Empty)) - : newLeft; - } - - if (rightNullable) - { - newRight = newRight is SqlConstantExpression - ? _sqlExpressionFactory.Constant(string.Empty) - : newRight is ColumnExpression || newRight is SqlParameterExpression - ? _sqlExpressionFactory.Coalesce(newRight, _sqlExpressionFactory.Constant(string.Empty)) - : newRight; - } - - return sqlBinaryExpression.Update(newLeft, newRight); - } - - if (sqlBinaryExpression.OperatorType == ExpressionType.Equal - || sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) - { - var leftConstantNull = newLeft is SqlConstantExpression leftConstant && leftConstant.Value == null; - var rightConstantNull = newRight is SqlConstantExpression rightConstant && rightConstant.Value == null; - - // a == null -> a IS NULL - // a != null -> a IS NOT NULL - if (rightConstantNull) - { - _isNullable = false; - _canOptimize = canOptimize; - - return sqlBinaryExpression.OperatorType == ExpressionType.Equal - ? _sqlExpressionFactory.IsNull(newLeft) - : _sqlExpressionFactory.IsNotNull(newLeft); - } - - // null == a -> a IS NULL - // null != a -> a IS NOT NULL - if (leftConstantNull) - { - _isNullable = false; - _canOptimize = canOptimize; - - return sqlBinaryExpression.OperatorType == ExpressionType.Equal - ? _sqlExpressionFactory.IsNull(newRight) - : _sqlExpressionFactory.IsNotNull(newRight); - } - - var leftUnary = newLeft as SqlUnaryExpression; - var rightUnary = newRight as SqlUnaryExpression; - - var leftNegated = leftUnary?.IsLogicalNot() == true; - var rightNegated = rightUnary?.IsLogicalNot() == true; - - if (leftNegated) - { - newLeft = leftUnary.Operand; - } - - if (rightNegated) - { - newRight = rightUnary.Operand; - } - - var leftIsNull = _sqlExpressionFactory.IsNull(newLeft); - var rightIsNull = _sqlExpressionFactory.IsNull(newRight); - - // optimized expansion which doesn't distinguish between null and false - if (canOptimize - && sqlBinaryExpression.OperatorType == ExpressionType.Equal - && !leftNegated - && !rightNegated) - { - // when we use optimized form, the result can still be nullable - if (leftNullable && rightNullable) - { - _isNullable = true; - _canOptimize = canOptimize; - - return _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.Equal(newLeft, newRight), - _sqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)); - } - - if ((leftNullable && !rightNullable) - || (!leftNullable && rightNullable)) - { - _isNullable = true; - _canOptimize = canOptimize; - - return _sqlExpressionFactory.Equal(newLeft, newRight); - } - } - - // doing a full null semantics rewrite - removing all nulls from truth table - // this will NOT be correct once we introduce simplified null semantics - _isNullable = false; - _canOptimize = canOptimize; - - if (sqlBinaryExpression.OperatorType == ExpressionType.Equal) - { - if (!leftNullable - && !rightNullable) - { - // a == b <=> !a == !b -> a == b - // !a == b <=> a == !b -> a != b - return leftNegated == rightNegated - ? _sqlExpressionFactory.Equal(newLeft, newRight) - : _sqlExpressionFactory.NotEqual(newLeft, newRight); - } - - if (leftNullable && rightNullable) - { - // ?a == ?b <=> !(?a) == !(?b) -> [(a == b) && (a != null && b != null)] || (a == null && b == null)) - // !(?a) == ?b <=> ?a == !(?b) -> [(a != b) && (a != null && b != null)] || (a == null && b == null) - return leftNegated == rightNegated - ? ExpandNullableEqualNullable(newLeft, newRight, leftIsNull, rightIsNull) - : ExpandNegatedNullableEqualNullable(newLeft, newRight, leftIsNull, rightIsNull); - } - - if (leftNullable && !rightNullable) - { - // ?a == b <=> !(?a) == !b -> (a == b) && (a != null) - // !(?a) == b <=> ?a == !b -> (a != b) && (a != null) - return leftNegated == rightNegated - ? ExpandNullableEqualNonNullable(newLeft, newRight, leftIsNull) - : ExpandNegatedNullableEqualNonNullable(newLeft, newRight, leftIsNull); - } - - if (rightNullable && !leftNullable) - { - // a == ?b <=> !a == !(?b) -> (a == b) && (b != null) - // !a == ?b <=> a == !(?b) -> (a != b) && (b != null) - return leftNegated == rightNegated - ? ExpandNullableEqualNonNullable(newLeft, newRight, rightIsNull) - : ExpandNegatedNullableEqualNonNullable(newLeft, newRight, rightIsNull); - } - } - - if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) - { - if (!leftNullable - && !rightNullable) - { - // a != b <=> !a != !b -> a != b - // !a != b <=> a != !b -> a == b - return leftNegated == rightNegated - ? _sqlExpressionFactory.NotEqual(newLeft, newRight) - : _sqlExpressionFactory.Equal(newLeft, newRight); - } - - if (leftNullable && rightNullable) - { - // ?a != ?b <=> !(?a) != !(?b) -> [(a != b) || (a == null || b == null)] && (a != null || b != null) - // !(?a) != ?b <=> ?a != !(?b) -> [(a == b) || (a == null || b == null)] && (a != null || b != null) - return leftNegated == rightNegated - ? ExpandNullableNotEqualNullable(newLeft, newRight, leftIsNull, rightIsNull) - : ExpandNegatedNullableNotEqualNullable(newLeft, newRight, leftIsNull, rightIsNull); - } - - if (leftNullable) - { - // ?a != b <=> !(?a) != !b -> (a != b) || (a == null) - // !(?a) != b <=> ?a != !b -> (a == b) || (a == null) - return leftNegated == rightNegated - ? ExpandNullableNotEqualNonNullable(newLeft, newRight, leftIsNull) - : ExpandNegatedNullableNotEqualNonNullable(newLeft, newRight, leftIsNull); - } - - if (rightNullable) - { - // a != ?b <=> !a != !(?b) -> (a != b) || (b == null) - // !a != ?b <=> a != !(?b) -> (a == b) || (b == null) - return leftNegated == rightNegated - ? ExpandNullableNotEqualNonNullable(newLeft, newRight, rightIsNull) - : ExpandNegatedNullableNotEqualNonNullable(newLeft, newRight, rightIsNull); - } - } - } - - _isNullable = leftNullable || rightNullable; - _canOptimize = canOptimize; - - return sqlBinaryExpression.Update(newLeft, newRight); - } - - protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) - { - Check.NotNull(sqlConstantExpression, nameof(sqlConstantExpression)); - - _isNullable = sqlConstantExpression.Value == null; - - return sqlConstantExpression; - } - - protected override Expression VisitSqlFragment(SqlFragmentExpression sqlFragmentExpression) - { - Check.NotNull(sqlFragmentExpression, nameof(sqlFragmentExpression)); - - return sqlFragmentExpression; - } - - protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) - { - Check.NotNull(sqlFunctionExpression, nameof(sqlFunctionExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - - var newInstance = (SqlExpression)Visit(sqlFunctionExpression.Instance); - var newArguments = new SqlExpression[sqlFunctionExpression.Arguments.Count]; - for (var i = 0; i < newArguments.Length; i++) - { - newArguments[i] = (SqlExpression)Visit(sqlFunctionExpression.Arguments[i]); - } - - _canOptimize = canOptimize; - - // TODO: #18555 - _isNullable = true; - - return sqlFunctionExpression.Update(newInstance, newArguments); - } - - protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) - { - Check.NotNull(sqlParameterExpression, nameof(sqlParameterExpression)); - - // at this point we assume every parameter is nullable, we will filter out the non-nullable ones once we know the actual values - _isNullable = true; - - return sqlParameterExpression; - } - - protected override Expression VisitSqlUnary(SqlUnaryExpression sqlCastExpression) - { - Check.NotNull(sqlCastExpression, nameof(sqlCastExpression)); - - _isNullable = false; - - var canOptimize = _canOptimize; - _canOptimize = false; - - var newOperand = (SqlExpression)Visit(sqlCastExpression.Operand); - - // result of IsNull/IsNotNull can never be null - if (sqlCastExpression.OperatorType == ExpressionType.Equal - || sqlCastExpression.OperatorType == ExpressionType.NotEqual) - { - _isNullable = false; - } - - _canOptimize = canOptimize; - - return sqlCastExpression.Update(newOperand); - } - - protected override Expression VisitTable(TableExpression tableExpression) - { - Check.NotNull(tableExpression, nameof(tableExpression)); - - return tableExpression; - } - - protected override Expression VisitUnion(UnionExpression unionExpression) - { - Check.NotNull(unionExpression, nameof(unionExpression)); - - var canOptimize = _canOptimize; - _canOptimize = false; - var source1 = (SelectExpression)Visit(unionExpression.Source1); - var source2 = (SelectExpression)Visit(unionExpression.Source2); - _canOptimize = canOptimize; - - return unionExpression.Update(source1, source2); - } - - private List FindNonNullableColumns(SqlExpression sqlExpression) - { - var result = new List(); - if (sqlExpression is SqlBinaryExpression sqlBinaryExpression) - { - if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) - { - if (sqlBinaryExpression.Left is ColumnExpression leftColumn - && leftColumn.IsNullable - && sqlBinaryExpression.Right is SqlConstantExpression rightConstant - && rightConstant.Value == null) - { - result.Add(leftColumn); - } - - if (sqlBinaryExpression.Right is ColumnExpression rightColumn - && rightColumn.IsNullable - && sqlBinaryExpression.Left is SqlConstantExpression leftConstant - && leftConstant.Value == null) - { - result.Add(rightColumn); - } - } - - if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso) - { - result.AddRange(FindNonNullableColumns(sqlBinaryExpression.Left)); - result.AddRange(FindNonNullableColumns(sqlBinaryExpression.Right)); - } - } - - return result; - } - - // ?a == ?b -> [(a == b) && (a != null && b != null)] || (a == null && b == null)) - // - // a | b | F1 = a == b | F2 = (a != null && b != null) | F3 = F1 && F2 | - // | | | | | - // 0 | 0 | 1 | 1 | 1 | - // 0 | 1 | 0 | 1 | 0 | - // 0 | N | N | 0 | 0 | - // 1 | 0 | 0 | 1 | 0 | - // 1 | 1 | 1 | 1 | 1 | - // 1 | N | N | 0 | 0 | - // N | 0 | N | 0 | 0 | - // N | 1 | N | 0 | 0 | - // N | N | N | 0 | 0 | - // - // a | b | F4 = (a == null && b == null) | Final = F3 OR F4 | - // | | | | - // 0 | 0 | 0 | 1 OR 0 = 1 | - // 0 | 1 | 0 | 0 OR 0 = 0 | - // 0 | N | 0 | 0 OR 0 = 0 | - // 1 | 0 | 0 | 0 OR 0 = 0 | - // 1 | 1 | 0 | 1 OR 0 = 1 | - // 1 | N | 0 | 0 OR 0 = 0 | - // N | 0 | 0 | 0 OR 0 = 0 | - // N | 1 | 0 | 0 OR 0 = 0 | - // N | N | 1 | 0 OR 1 = 1 | - private SqlBinaryExpression ExpandNullableEqualNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull, SqlExpression rightIsNull) - => _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.Equal(left, right), - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.Not(leftIsNull), - _sqlExpressionFactory.Not(rightIsNull))), - _sqlExpressionFactory.AndAlso( - leftIsNull, - rightIsNull)); - - // !(?a) == ?b -> [(a != b) && (a != null && b != null)] || (a == null && b == null) - // - // a | b | F1 = a != b | F2 = (a != null && b != null) | F3 = F1 && F2 | - // | | | | | - // 0 | 0 | 0 | 1 | 0 | - // 0 | 1 | 1 | 1 | 1 | - // 0 | N | N | 0 | 0 | - // 1 | 0 | 1 | 1 | 1 | - // 1 | 1 | 0 | 1 | 0 | - // 1 | N | N | 0 | 0 | - // N | 0 | N | 0 | 0 | - // N | 1 | N | 0 | 0 | - // N | N | N | 0 | 0 | - // - // a | b | F4 = (a == null && b == null) | Final = F3 OR F4 | - // | | | | - // 0 | 0 | 0 | 0 OR 0 = 0 | - // 0 | 1 | 0 | 1 OR 0 = 1 | - // 0 | N | 0 | 0 OR 0 = 0 | - // 1 | 0 | 0 | 1 OR 0 = 1 | - // 1 | 1 | 0 | 0 OR 0 = 0 | - // 1 | N | 0 | 0 OR 0 = 0 | - // N | 0 | 0 | 0 OR 0 = 0 | - // N | 1 | 0 | 0 OR 0 = 0 | - // N | N | 1 | 0 OR 1 = 1 | - private SqlBinaryExpression ExpandNegatedNullableEqualNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull, SqlExpression rightIsNull) - => _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.NotEqual(left, right), - _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.Not(leftIsNull), - _sqlExpressionFactory.Not(rightIsNull))), - _sqlExpressionFactory.AndAlso( - leftIsNull, - rightIsNull)); - - // ?a == b -> (a == b) && (a != null) - // - // a | b | F1 = a == b | F2 = (a != null) | Final = F1 && F2 | - // | | | | | - // 0 | 0 | 1 | 1 | 1 | - // 0 | 1 | 0 | 1 | 0 | - // 1 | 0 | 0 | 1 | 0 | - // 1 | 1 | 1 | 1 | 1 | - // N | 0 | N | 0 | 0 | - // N | 1 | N | 0 | 0 | - private SqlBinaryExpression ExpandNullableEqualNonNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull) - => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.Equal(left, right), - _sqlExpressionFactory.Not(leftIsNull)); - - // !(?a) == b -> (a != b) && (a != null) - // - // a | b | F1 = a != b | F2 = (a != null) | Final = F1 && F2 | - // | | | | | - // 0 | 0 | 0 | 1 | 0 | - // 0 | 1 | 1 | 1 | 1 | - // 1 | 0 | 1 | 1 | 1 | - // 1 | 1 | 0 | 1 | 0 | - // N | 0 | N | 0 | 0 | - // N | 1 | N | 0 | 0 | - private SqlBinaryExpression ExpandNegatedNullableEqualNonNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull) - => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.NotEqual(left, right), - _sqlExpressionFactory.Not(leftIsNull)); - - // ?a != ?b -> [(a != b) || (a == null || b == null)] && (a != null || b != null) - // - // a | b | F1 = a != b | F2 = (a == null || b == null) | F3 = F1 || F2 | - // | | | | | - // 0 | 0 | 0 | 0 | 0 | - // 0 | 1 | 1 | 0 | 1 | - // 0 | N | N | 1 | 1 | - // 1 | 0 | 1 | 0 | 1 | - // 1 | 1 | 0 | 0 | 0 | - // 1 | N | N | 1 | 1 | - // N | 0 | N | 1 | 1 | - // N | 1 | N | 1 | 1 | - // N | N | N | 1 | 1 | - // - // a | b | F4 = (a != null || b != null) | Final = F3 && F4 | - // | | | | - // 0 | 0 | 1 | 0 && 1 = 0 | - // 0 | 1 | 1 | 1 && 1 = 1 | - // 0 | N | 1 | 1 && 1 = 1 | - // 1 | 0 | 1 | 1 && 1 = 1 | - // 1 | 1 | 1 | 0 && 1 = 0 | - // 1 | N | 1 | 1 && 1 = 1 | - // N | 0 | 1 | 1 && 1 = 1 | - // N | 1 | 1 | 1 && 1 = 1 | - // N | N | 0 | 1 && 0 = 0 | - private SqlBinaryExpression ExpandNullableNotEqualNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull, SqlExpression rightIsNull) - => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.NotEqual(left, right), - _sqlExpressionFactory.OrElse( - leftIsNull, - rightIsNull)), - _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.Not(leftIsNull), - _sqlExpressionFactory.Not(rightIsNull))); - - // !(?a) != ?b -> [(a == b) || (a == null || b == null)] && (a != null || b != null) - // - // a | b | F1 = a == b | F2 = (a == null || b == null) | F3 = F1 || F2 | - // | | | | | - // 0 | 0 | 1 | 0 | 1 | - // 0 | 1 | 0 | 0 | 0 | - // 0 | N | N | 1 | 1 | - // 1 | 0 | 0 | 0 | 0 | - // 1 | 1 | 1 | 0 | 1 | - // 1 | N | N | 1 | 1 | - // N | 0 | N | 1 | 1 | - // N | 1 | N | 1 | 1 | - // N | N | N | 1 | 1 | - // - // a | b | F4 = (a != null || b != null) | Final = F3 && F4 | - // | | | | - // 0 | 0 | 1 | 1 && 1 = 1 | - // 0 | 1 | 1 | 0 && 1 = 0 | - // 0 | N | 1 | 1 && 1 = 1 | - // 1 | 0 | 1 | 0 && 1 = 0 | - // 1 | 1 | 1 | 1 && 1 = 1 | - // 1 | N | 1 | 1 && 1 = 1 | - // N | 0 | 1 | 1 && 1 = 1 | - // N | 1 | 1 | 1 && 1 = 1 | - // N | N | 0 | 1 && 0 = 0 | - private SqlBinaryExpression ExpandNegatedNullableNotEqualNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull, SqlExpression rightIsNull) - => _sqlExpressionFactory.AndAlso( - _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.Equal(left, right), - _sqlExpressionFactory.OrElse( - leftIsNull, - rightIsNull)), - _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.Not(leftIsNull), - _sqlExpressionFactory.Not(rightIsNull))); - - // ?a != b -> (a != b) || (a == null) - // - // a | b | F1 = a != b | F2 = (a == null) | Final = F1 OR F2 | - // | | | | | - // 0 | 0 | 0 | 0 | 0 | - // 0 | 1 | 1 | 0 | 1 | - // 1 | 0 | 1 | 0 | 1 | - // 1 | 1 | 0 | 0 | 0 | - // N | 0 | N | 1 | 1 | - // N | 1 | N | 1 | 1 | - private SqlBinaryExpression ExpandNullableNotEqualNonNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull) - => _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.NotEqual(left, right), - leftIsNull); - - // !(?a) != b -> (a == b) || (a == null) - // - // a | b | F1 = a == b | F2 = (a == null) | F3 = F1 OR F2 | - // | | | | | - // 0 | 0 | 1 | 0 | 1 | - // 0 | 1 | 0 | 0 | 0 | - // 1 | 0 | 0 | 0 | 0 | - // 1 | 1 | 1 | 0 | 1 | - // N | 0 | N | 1 | 1 | - // N | 1 | N | 1 | 1 | - private SqlBinaryExpression ExpandNegatedNullableNotEqualNonNullable( - SqlExpression left, SqlExpression right, SqlExpression leftIsNull) - => _sqlExpressionFactory.OrElse( - _sqlExpressionFactory.Equal(left, right), - leftIsNull); - } -} diff --git a/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs deleted file mode 100644 index 1e555aa198d..00000000000 --- a/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs +++ /dev/null @@ -1,510 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Linq; -using System.Linq.Expressions; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Utilities; - -namespace Microsoft.EntityFrameworkCore.Query.Internal -{ - public class SqlExpressionOptimizingExpressionVisitor : ExpressionVisitor - { - private readonly bool _useRelationalNulls; - - private static bool TryNegate(ExpressionType expressionType, out ExpressionType result) - { - var negated = expressionType switch - { - ExpressionType.AndAlso => ExpressionType.OrElse, - ExpressionType.OrElse => ExpressionType.AndAlso, - ExpressionType.Equal => ExpressionType.NotEqual, - ExpressionType.NotEqual => ExpressionType.Equal, - ExpressionType.GreaterThan => ExpressionType.LessThanOrEqual, - ExpressionType.GreaterThanOrEqual => ExpressionType.LessThan, - ExpressionType.LessThan => ExpressionType.GreaterThanOrEqual, - ExpressionType.LessThanOrEqual => ExpressionType.GreaterThan, - _ => (ExpressionType?)null - }; - - result = negated ?? default; - - return negated.HasValue; - } - - public SqlExpressionOptimizingExpressionVisitor([NotNull] ISqlExpressionFactory sqlExpressionFactory, bool useRelationalNulls) - { - SqlExpressionFactory = sqlExpressionFactory; - _useRelationalNulls = useRelationalNulls; - } - - protected virtual ISqlExpressionFactory SqlExpressionFactory { get; } - - protected override Expression VisitExtension(Expression extensionExpression) - { - Check.NotNull(extensionExpression, nameof(extensionExpression)); - - return extensionExpression switch - { - SqlUnaryExpression sqlUnaryExpression => VisitSqlUnaryExpression(sqlUnaryExpression), - SqlBinaryExpression sqlBinaryExpression => VisitSqlBinaryExpression(sqlBinaryExpression), - SelectExpression selectExpression => VisitSelectExpression(selectExpression), - _ => base.VisitExtension(extensionExpression), - }; - } - - private Expression VisitSelectExpression(SelectExpression selectExpression) - { - var newExpression = base.VisitExtension(selectExpression); - - // if predicate is optimized to true, we can simply remove it - if (newExpression is SelectExpression newSelectExpression) - { - var changed = false; - var newPredicate = newSelectExpression.Predicate; - var newHaving = newSelectExpression.Having; - if (newSelectExpression.Predicate is SqlConstantExpression predicateConstantExpression - && predicateConstantExpression.Value is bool predicateBoolValue - && predicateBoolValue) - { - newPredicate = null; - changed = true; - } - - if (newSelectExpression.Having is SqlConstantExpression havingConstantExpression - && havingConstantExpression.Value is bool havingBoolValue - && havingBoolValue) - { - newHaving = null; - changed = true; - } - - return changed - ? newSelectExpression.Update( - newSelectExpression.Projection.ToList(), - newSelectExpression.Tables.ToList(), - newPredicate, - newSelectExpression.GroupBy.ToList(), - newHaving, - newSelectExpression.Orderings.ToList(), - newSelectExpression.Limit, - newSelectExpression.Offset, - newSelectExpression.IsDistinct, - newSelectExpression.Alias) - : newSelectExpression; - } - - return newExpression; - } - - protected virtual Expression VisitSqlUnaryExpression([NotNull] SqlUnaryExpression sqlUnaryExpression) - { - var newOperand = (SqlExpression)Visit(sqlUnaryExpression.Operand); - - return SimplifyUnaryExpression( - sqlUnaryExpression.OperatorType, - newOperand, - sqlUnaryExpression.Type, - sqlUnaryExpression.TypeMapping); - } - - private SqlExpression SimplifyUnaryExpression( - ExpressionType operatorType, - SqlExpression operand, - Type type, - RelationalTypeMapping typeMapping) - { - switch (operatorType) - { - case ExpressionType.Not - when type == typeof(bool) - || type == typeof(bool?): - { - switch (operand) - { - // !(true) -> false - // !(false) -> true - case SqlConstantExpression constantOperand - when constantOperand.Value is bool value: - { - return SqlExpressionFactory.Constant(!value, typeMapping); - } - - case InExpression inOperand: - return inOperand.Negate(); - - case SqlUnaryExpression unaryOperand: - switch (unaryOperand.OperatorType) - { - // !(!a) -> a - case ExpressionType.Not: - return unaryOperand.Operand; - - //!(a IS NULL) -> a IS NOT NULL - case ExpressionType.Equal: - return SqlExpressionFactory.IsNotNull(unaryOperand.Operand); - - //!(a IS NOT NULL) -> a IS NULL - case ExpressionType.NotEqual: - return SqlExpressionFactory.IsNull(unaryOperand.Operand); - } - - break; - - case SqlBinaryExpression binaryOperand: - { - // De Morgan's - if (binaryOperand.OperatorType == ExpressionType.AndAlso - || binaryOperand.OperatorType == ExpressionType.OrElse) - { - var newLeft = SimplifyUnaryExpression(ExpressionType.Not, binaryOperand.Left, type, typeMapping); - var newRight = SimplifyUnaryExpression(ExpressionType.Not, binaryOperand.Right, type, typeMapping); - - return SimplifyLogicalSqlBinaryExpression( - binaryOperand.OperatorType == ExpressionType.AndAlso - ? ExpressionType.OrElse - : ExpressionType.AndAlso, - newLeft, - newRight, - binaryOperand.TypeMapping); - } - - // those optimizations are only valid in 2-value logic - // they are safe to do here because if we apply null semantics - // because null semantics removes possibility of nulls in the tree when the comparison is wrapped around NOT - if (!_useRelationalNulls - && TryNegate(binaryOperand.OperatorType, out var negated)) - { - return SimplifyBinaryExpression( - negated, - binaryOperand.Left, - binaryOperand.Right, - binaryOperand.TypeMapping); - } - } - break; - } - break; - } - - case ExpressionType.Equal: - case ExpressionType.NotEqual: - return SimplifyNullNotNullExpression( - operatorType, - operand, - type, - typeMapping); - } - - return SqlExpressionFactory.MakeUnary(operatorType, operand, type, typeMapping); - } - - private SqlExpression SimplifyNullNotNullExpression( - ExpressionType operatorType, - SqlExpression operand, - Type type, - RelationalTypeMapping typeMapping) - { - switch (operatorType) - { - case ExpressionType.Equal: - case ExpressionType.NotEqual: - switch (operand) - { - case SqlConstantExpression constantOperand: - return SqlExpressionFactory.Constant( - operatorType == ExpressionType.Equal - ? constantOperand.Value == null - : constantOperand.Value != null, - typeMapping); - - case ColumnExpression columnOperand - when !columnOperand.IsNullable: - return SqlExpressionFactory.Constant(operatorType == ExpressionType.NotEqual, typeMapping); - - case SqlUnaryExpression sqlUnaryOperand: - if (sqlUnaryOperand.OperatorType == ExpressionType.Convert - || sqlUnaryOperand.OperatorType == ExpressionType.Not - || sqlUnaryOperand.OperatorType == ExpressionType.Negate) - { - // op(a) is null -> a is null - // op(a) is not null -> a is not null - return SimplifyNullNotNullExpression(operatorType, sqlUnaryOperand.Operand, type, typeMapping); - } - - if (sqlUnaryOperand.OperatorType == ExpressionType.Equal - || sqlUnaryOperand.OperatorType == ExpressionType.NotEqual) - { - // (a is null) is null -> false - // (a is not null) is null -> false - // (a is null) is not null -> true - // (a is not null) is not null -> true - return SqlExpressionFactory.Constant(operatorType == ExpressionType.NotEqual, typeMapping); - } - break; - - case SqlBinaryExpression sqlBinaryOperand: - // in general: - // binaryOp(a, b) == null -> a == null || b == null - // binaryOp(a, b) != null -> a != null && b != null - // for coalesce: - // (a ?? b) == null -> a == null && b == null - // (a ?? b) != null -> a != null || b != null - // for AndAlso, OrElse we can't do this optimization - // we could do something like this, but it seems too complicated: - // (a && b) == null -> a == null && b != 0 || a != 0 && b == null - if (sqlBinaryOperand.OperatorType != ExpressionType.AndAlso - && sqlBinaryOperand.OperatorType != ExpressionType.OrElse) - { - var newLeft = SimplifyNullNotNullExpression(operatorType, sqlBinaryOperand.Left, typeof(bool), typeMapping); - var newRight = SimplifyNullNotNullExpression(operatorType, sqlBinaryOperand.Right, typeof(bool), typeMapping); - - return sqlBinaryOperand.OperatorType == ExpressionType.Coalesce - ? SimplifyLogicalSqlBinaryExpression( - operatorType == ExpressionType.Equal - ? ExpressionType.AndAlso - : ExpressionType.OrElse, - newLeft, - newRight, - typeMapping) - : SimplifyLogicalSqlBinaryExpression( - operatorType == ExpressionType.Equal - ? ExpressionType.OrElse - : ExpressionType.AndAlso, - newLeft, - newRight, - typeMapping); - } - break; - } - break; - } - - return SqlExpressionFactory.MakeUnary(operatorType, operand, type, typeMapping); - } - - protected virtual Expression VisitSqlBinaryExpression([NotNull] SqlBinaryExpression sqlBinaryExpression) - { - var newLeft = (SqlExpression)Visit(sqlBinaryExpression.Left); - var newRight = (SqlExpression)Visit(sqlBinaryExpression.Right); - - return SimplifyBinaryExpression( - sqlBinaryExpression.OperatorType, - newLeft, - newRight, - sqlBinaryExpression.TypeMapping); - } - - private SqlExpression SimplifyBinaryExpression( - ExpressionType operatorType, - SqlExpression left, - SqlExpression right, - RelationalTypeMapping typeMapping) - { - switch (operatorType) - { - case ExpressionType.AndAlso: - case ExpressionType.OrElse: - var leftUnary = left as SqlUnaryExpression; - var rightUnary = right as SqlUnaryExpression; - if (leftUnary != null - && rightUnary != null - && (leftUnary.OperatorType == ExpressionType.Equal || leftUnary.OperatorType == ExpressionType.NotEqual) - && (rightUnary.OperatorType == ExpressionType.Equal || rightUnary.OperatorType == ExpressionType.NotEqual) - && leftUnary.Operand.Equals(rightUnary.Operand)) - { - // a is null || a is null -> a is null - // a is not null || a is not null -> a is not null - // a is null && a is null -> a is null - // a is not null && a is not null -> a is not null - // a is null || a is not null -> true - // a is null && a is not null -> false - return leftUnary.OperatorType == rightUnary.OperatorType - ? (SqlExpression)leftUnary - : SqlExpressionFactory.Constant(operatorType == ExpressionType.OrElse, typeMapping); - } - - return SimplifyLogicalSqlBinaryExpression( - operatorType, - left, - right, - typeMapping); - - case ExpressionType.Equal: - case ExpressionType.NotEqual: - var leftConstant = left as SqlConstantExpression; - var rightConstant = right as SqlConstantExpression; - var leftNullConstant = leftConstant != null && leftConstant.Value == null; - var rightNullConstant = rightConstant != null && rightConstant.Value == null; - if (leftNullConstant || rightNullConstant) - { - return SimplifyNullComparisonExpression( - operatorType, - left, - right, - leftNullConstant, - rightNullConstant, - typeMapping); - } - - var leftBoolConstant = left.Type == typeof(bool) ? leftConstant : null; - var rightBoolConstant = right.Type == typeof(bool) ? rightConstant : null; - if (leftBoolConstant != null || rightBoolConstant != null) - { - return SimplifyBoolConstantComparisonExpression( - operatorType, - left, - right, - leftBoolConstant, - rightBoolConstant, - typeMapping); - } - - // only works when a is not nullable - // a == a -> true - // a != a -> false - if ((left is LikeExpression - || left is ColumnExpression columnExpression && !columnExpression.IsNullable) - && left.Equals(right)) - { - return SqlExpressionFactory.Constant(operatorType == ExpressionType.Equal, typeMapping); - } - - break; - } - - return SqlExpressionFactory.MakeBinary(operatorType, left, right, typeMapping); - } - - protected virtual SqlExpression SimplifyNullComparisonExpression( - ExpressionType operatorType, - [NotNull] SqlExpression left, - [NotNull] SqlExpression right, - bool leftNull, - bool rightNull, - [CanBeNull] RelationalTypeMapping typeMapping) - { - if ((operatorType == ExpressionType.Equal || operatorType == ExpressionType.NotEqual) - && (leftNull || rightNull)) - { - if (leftNull && rightNull) - { - return SqlExpressionFactory.Constant(operatorType == ExpressionType.Equal, typeMapping); - } - - if (leftNull) - { - return SimplifyNullNotNullExpression(operatorType, right, typeof(bool), typeMapping); - } - - if (rightNull) - { - return SimplifyNullNotNullExpression(operatorType, left, typeof(bool), typeMapping); - } - } - - return SqlExpressionFactory.MakeBinary(operatorType, left, right, typeMapping); - } - - private SqlExpression SimplifyBoolConstantComparisonExpression( - ExpressionType operatorType, - SqlExpression left, - SqlExpression right, - SqlConstantExpression leftBoolConstant, - SqlConstantExpression rightBoolConstant, - RelationalTypeMapping typeMapping) - { - if (leftBoolConstant != null && rightBoolConstant != null) - { - return operatorType == ExpressionType.Equal - ? SqlExpressionFactory.Constant((bool)leftBoolConstant.Value == (bool)rightBoolConstant.Value, typeMapping) - : SqlExpressionFactory.Constant((bool)leftBoolConstant.Value != (bool)rightBoolConstant.Value, typeMapping); - } - - if (rightBoolConstant != null - && CanOptimize(left)) - { - // a == true -> a - // a == false -> !a - // a != true -> !a - // a != false -> a - // only correct when f(x) can't be null - return operatorType == ExpressionType.Equal - ? (bool)rightBoolConstant.Value - ? left - : SimplifyUnaryExpression(ExpressionType.Not, left, typeof(bool), typeMapping) - : (bool)rightBoolConstant.Value - ? SimplifyUnaryExpression(ExpressionType.Not, left, typeof(bool), typeMapping) - : left; - } - - if (leftBoolConstant != null - && CanOptimize(right)) - { - // true == a -> a - // false == a -> !a - // true != a -> !a - // false != a -> a - // only correct when a can't be null - return operatorType == ExpressionType.Equal - ? (bool)leftBoolConstant.Value - ? right - : SimplifyUnaryExpression(ExpressionType.Not, right, typeof(bool), typeMapping) - : (bool)leftBoolConstant.Value - ? SimplifyUnaryExpression(ExpressionType.Not, right, typeof(bool), typeMapping) - : right; - } - - return SqlExpressionFactory.MakeBinary(operatorType, left, right, typeMapping); - - static bool CanOptimize(SqlExpression operand) - => operand is LikeExpression - || (operand is SqlUnaryExpression sqlUnary - && (sqlUnary.OperatorType == ExpressionType.Equal - || sqlUnary.OperatorType == ExpressionType.NotEqual - // TODO: #18689 - /*|| sqlUnary.OperatorType == ExpressionType.Not*/)); - } - - private SqlExpression SimplifyLogicalSqlBinaryExpression( - ExpressionType operatorType, - SqlExpression left, - SqlExpression right, - RelationalTypeMapping typeMapping) - { - // true && a -> a - // true || a -> true - // false && a -> false - // false || a -> a - if (left is SqlConstantExpression newLeftConstant) - { - return operatorType == ExpressionType.AndAlso - ? (bool)newLeftConstant.Value - ? right - : newLeftConstant - : (bool)newLeftConstant.Value - ? newLeftConstant - : right; - } - else if (right is SqlConstantExpression newRightConstant) - { - // a && true -> a - // a || true -> true - // a && false -> false - // a || false -> a - return operatorType == ExpressionType.AndAlso - ? (bool)newRightConstant.Value - ? left - : newRightConstant - : (bool)newRightConstant.Value - ? newRightConstant - : left; - } - - return SqlExpressionFactory.MakeBinary(operatorType, left, right, typeMapping); - } - } -} diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs index 5610cf29666..d879ef37a13 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs @@ -1,27 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections; using System.Collections.Generic; -using System.Data.Common; -using System.Linq.Expressions; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query { - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// public class RelationalParameterBasedQueryTranslationPostprocessor { public RelationalParameterBasedQueryTranslationPostprocessor( @@ -46,300 +32,24 @@ public virtual (SelectExpression selectExpression, bool canCache) Optimize( Check.NotNull(parametersValues, nameof(parametersValues)); var canCache = true; + var (sqlExpressionOptimized, optimizerCanCache) = new SqlExpressionOptimizingExpressionVisitor( + UseRelationalNulls, + Dependencies.SqlExpressionFactory, + parametersValues).HandleNullability(selectExpression); - var inExpressionOptimized = new InExpressionValuesExpandingExpressionVisitor( - Dependencies.SqlExpressionFactory, parametersValues).Visit(selectExpression); - - if (!ReferenceEquals(selectExpression, inExpressionOptimized)) - { - canCache = false; - } - - var nullParametersOptimized = new ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( - Dependencies.SqlExpressionFactory, UseRelationalNulls, parametersValues).Visit(inExpressionOptimized); + canCache &= optimizerCanCache; var fromSqlParameterOptimized = new FromSqlParameterApplyingExpressionVisitor( Dependencies.SqlExpressionFactory, Dependencies.ParameterNameGeneratorFactory.Create(), - parametersValues).Visit(nullParametersOptimized); + parametersValues).Visit(sqlExpressionOptimized); - if (!ReferenceEquals(nullParametersOptimized, fromSqlParameterOptimized)) + if (!ReferenceEquals(sqlExpressionOptimized, fromSqlParameterOptimized)) { canCache = false; } return (selectExpression: (SelectExpression)fromSqlParameterOptimized, canCache); } - - private sealed class ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor : SqlExpressionOptimizingExpressionVisitor - { - private readonly IReadOnlyDictionary _parametersValues; - - public ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, - bool useRelationalNulls, - IReadOnlyDictionary parametersValues) - : base(sqlExpressionFactory, useRelationalNulls) - { - _parametersValues = parametersValues; - } - - protected override Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression) - { - var result = base.VisitSqlUnaryExpression(sqlUnaryExpression); - if (result is SqlUnaryExpression newUnaryExpression - && newUnaryExpression.Operand is SqlParameterExpression parameterOperand) - { - var parameterValue = _parametersValues[parameterOperand.Name]; - if (sqlUnaryExpression.OperatorType == ExpressionType.Equal) - { - return SqlExpressionFactory.Constant(parameterValue == null, sqlUnaryExpression.TypeMapping); - } - - if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual) - { - return SqlExpressionFactory.Constant(parameterValue != null, sqlUnaryExpression.TypeMapping); - } - } - - return result; - } - - protected override Expression VisitSqlBinaryExpression(SqlBinaryExpression sqlBinaryExpression) - { - var result = base.VisitSqlBinaryExpression(sqlBinaryExpression); - if (result is SqlBinaryExpression sqlBinaryResult) - { - var leftNullParameter = sqlBinaryResult.Left is SqlParameterExpression leftParameter - && _parametersValues[leftParameter.Name] == null; - - var rightNullParameter = sqlBinaryResult.Right is SqlParameterExpression rightParameter - && _parametersValues[rightParameter.Name] == null; - - if ((sqlBinaryResult.OperatorType == ExpressionType.Equal || sqlBinaryResult.OperatorType == ExpressionType.NotEqual) - && (leftNullParameter || rightNullParameter)) - { - return SimplifyNullComparisonExpression( - sqlBinaryResult.OperatorType, - sqlBinaryResult.Left, - sqlBinaryResult.Right, - leftNullParameter, - rightNullParameter, - sqlBinaryResult.TypeMapping); - } - } - - return result; - } - } - - private sealed class InExpressionValuesExpandingExpressionVisitor : ExpressionVisitor - { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IReadOnlyDictionary _parametersValues; - - public InExpressionValuesExpandingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary parametersValues) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parametersValues = parametersValues; - } - - public override Expression Visit(Expression expression) - { - if (expression is InExpression inExpression - && inExpression.Values != null) - { - var inValues = new List(); - var hasNullValue = false; - RelationalTypeMapping typeMapping = null; - - switch (inExpression.Values) - { - case SqlConstantExpression sqlConstant: - { - typeMapping = sqlConstant.TypeMapping; - var values = (IEnumerable)sqlConstant.Value; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add(value); - } - - break; - } - - case SqlParameterExpression sqlParameter: - { - typeMapping = sqlParameter.TypeMapping; - var values = (IEnumerable)_parametersValues[sqlParameter.Name]; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add(value); - } - - break; - } - } - - var updatedInExpression = inValues.Count > 0 - ? _sqlExpressionFactory.In( - (SqlExpression)Visit(inExpression.Item), - _sqlExpressionFactory.Constant(inValues, typeMapping), - inExpression.IsNegated) - : null; - - var nullCheckExpression = hasNullValue - ? inExpression.IsNegated - ? _sqlExpressionFactory.IsNotNull(inExpression.Item) - : _sqlExpressionFactory.IsNull(inExpression.Item) - : null; - - if (updatedInExpression != null - && nullCheckExpression != null) - { - return inExpression.IsNegated - ? _sqlExpressionFactory.AndAlso(updatedInExpression, nullCheckExpression) - : _sqlExpressionFactory.OrElse(updatedInExpression, nullCheckExpression); - } - - if (updatedInExpression == null - && nullCheckExpression == null) - { - return _sqlExpressionFactory.Equal( - _sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(inExpression.IsNegated)); - } - - return (SqlExpression)updatedInExpression ?? nullCheckExpression; - } - - return base.Visit(expression); - } - } - - private sealed class FromSqlParameterApplyingExpressionVisitor : ExpressionVisitor - { - private readonly IDictionary _visitedFromSqlExpressions - = new Dictionary(ReferenceEqualityComparer.Instance); - - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly ParameterNameGenerator _parameterNameGenerator; - private readonly IReadOnlyDictionary _parametersValues; - - public FromSqlParameterApplyingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, - ParameterNameGenerator parameterNameGenerator, - IReadOnlyDictionary parametersValues) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parameterNameGenerator = parameterNameGenerator; - _parametersValues = parametersValues; - } - - public override Expression Visit(Expression expression) - { - if (expression is FromSqlExpression fromSql) - { - if (!_visitedFromSqlExpressions.TryGetValue(fromSql, out var updatedFromSql)) - { - switch (fromSql.Arguments) - { - case ParameterExpression parameterExpression: - var parameterValues = (object[])_parametersValues[parameterExpression.Name]; - - var subParameters = new List(parameterValues.Length); - // ReSharper disable once ForCanBeConvertedToForeach - for (var i = 0; i < parameterValues.Length; i++) - { - var parameterName = _parameterNameGenerator.GenerateNext(); - if (parameterValues[i] is DbParameter dbParameter) - { - if (string.IsNullOrEmpty(dbParameter.ParameterName)) - { - dbParameter.ParameterName = parameterName; - } - else - { - parameterName = dbParameter.ParameterName; - } - - subParameters.Add(new RawRelationalParameter(parameterName, dbParameter)); - } - else - { - subParameters.Add( - new TypeMappedRelationalParameter( - parameterName, - parameterName, - _sqlExpressionFactory.GetTypeMappingForValue(parameterValues[i]), - parameterValues[i]?.GetType().IsNullableType())); - } - } - - updatedFromSql = new FromSqlExpression( - fromSql.Sql, - Expression.Constant( - new CompositeRelationalParameter( - parameterExpression.Name, - subParameters)), - fromSql.Alias); - - _visitedFromSqlExpressions[fromSql] = updatedFromSql; - break; - - case ConstantExpression constantExpression: - var existingValues = (object[])constantExpression.Value; - var constantValues = new object[existingValues.Length]; - for (var i = 0; i < existingValues.Length; i++) - { - var value = existingValues[i]; - if (value is DbParameter dbParameter) - { - var parameterName = _parameterNameGenerator.GenerateNext(); - if (string.IsNullOrEmpty(dbParameter.ParameterName)) - { - dbParameter.ParameterName = parameterName; - } - else - { - parameterName = dbParameter.ParameterName; - } - - constantValues[i] = new RawRelationalParameter(parameterName, dbParameter); - } - else - { - constantValues[i] = _sqlExpressionFactory.Constant( - value, _sqlExpressionFactory.GetTypeMappingForValue(value)); - } - } - - updatedFromSql = new FromSqlExpression( - fromSql.Sql, - Expression.Constant(constantValues, typeof(object[])), - fromSql.Alias); - - _visitedFromSqlExpressions[fromSql] = updatedFromSql; - break; - } - } - - return updatedFromSql; - } - - return base.Visit(expression); - } - } } } diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs index d25ecdf8a09..0c8acdb16ca 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs @@ -11,8 +11,6 @@ namespace Microsoft.EntityFrameworkCore.Query { public class RelationalQueryTranslationPostprocessor : QueryTranslationPostprocessor { - private readonly SqlExpressionOptimizingExpressionVisitor _sqlExpressionOptimizingExpressionVisitor; - public RelationalQueryTranslationPostprocessor( [NotNull] QueryTranslationPostprocessorDependencies dependencies, [NotNull] RelationalQueryTranslationPostprocessorDependencies relationalDependencies, @@ -25,8 +23,6 @@ public RelationalQueryTranslationPostprocessor( RelationalDependencies = relationalDependencies; UseRelationalNulls = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).UseRelationalNulls; SqlExpressionFactory = relationalDependencies.SqlExpressionFactory; - _sqlExpressionOptimizingExpressionVisitor - = new SqlExpressionOptimizingExpressionVisitor(SqlExpressionFactory, UseRelationalNulls); } protected virtual RelationalQueryTranslationPostprocessorDependencies RelationalDependencies { get; } @@ -42,22 +38,12 @@ public override Expression Process(Expression query) query = new CollectionJoinApplyingExpressionVisitor().Visit(query); query = new TableAliasUniquifyingExpressionVisitor().Visit(query); query = new CaseWhenFlatteningExpressionVisitor(SqlExpressionFactory).Visit(query); - - if (!UseRelationalNulls) - { - query = new NullSemanticsRewritingExpressionVisitor(SqlExpressionFactory).Visit(query); - } - query = OptimizeSqlExpression(query); return query; } protected virtual Expression OptimizeSqlExpression([NotNull] Expression query) - { - Check.NotNull(query, nameof(query)); - - return _sqlExpressionOptimizingExpressionVisitor.Visit(query); - } + => query; } } diff --git a/src/EFCore.Relational/Query/SqlExpressionOptimizingExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionOptimizingExpressionVisitor.cs new file mode 100644 index 00000000000..3ad8e47fbb9 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressionOptimizingExpressionVisitor.cs @@ -0,0 +1,1723 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class SqlExpressionOptimizingExpressionVisitor : SqlExpressionVisitor + { + protected virtual bool UseRelationalNulls { get; } + protected virtual ISqlExpressionFactory SqlExpressionFactory { get; } + protected virtual IReadOnlyDictionary ParameterValues { get; } + protected virtual List NonNullableColumns { get; } = new List(); + + protected virtual bool IsNullable { get; set; } + protected virtual bool CanOptimize { get; set; } + protected virtual bool CanCache { get; set; } + + public SqlExpressionOptimizingExpressionVisitor( + bool useRelationalNulls, + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + [NotNull] IReadOnlyDictionary parameterValues) + { + UseRelationalNulls = useRelationalNulls; + SqlExpressionFactory = sqlExpressionFactory; + ParameterValues = parameterValues; + + CanOptimize = true; + CanCache = true; + } + + private void RestoreNonNullableColumnsList(int counter) + { + if (counter < NonNullableColumns.Count) + { + NonNullableColumns.RemoveRange(counter, NonNullableColumns.Count - counter); + } + } + + public virtual (SelectExpression selectExpression, bool canCache) HandleNullability([NotNull] SelectExpression selectExpression) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + + var result = (SelectExpression)Visit(selectExpression); + + return (selectExpression: result, canCache: CanCache); + } + + protected override Expression VisitCase(CaseExpression caseExpression) + { + Check.NotNull(caseExpression, nameof(caseExpression)); + + IsNullable = false; + // if there is no 'else' there is a possibility of null, when none of the conditions are met + // otherwise the result is nullable if any of the WhenClause results OR ElseResult is nullable + var isNullable = caseExpression.ElseResult == null; + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + var testIsCondition = caseExpression.Operand == null; + CanOptimize = false; + var newOperand = (SqlExpression)Visit(caseExpression.Operand); + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + var newWhenClauses = new List(); + foreach (var whenClause in caseExpression.WhenClauses) + { + CanOptimize = testIsCondition; + + var newTest = (SqlExpression)Visit(whenClause.Test); + CanOptimize = false; + IsNullable = false; + var newResult = (SqlExpression)Visit(whenClause.Result); + isNullable |= IsNullable; + newWhenClauses.Add(new CaseWhenClause(newTest, newResult)); + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + CanOptimize = false; + var newElseResult = (SqlExpression)Visit(caseExpression.ElseResult); + IsNullable |= isNullable; + CanOptimize = canOptimize; + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return caseExpression.Update(newOperand, newWhenClauses, newElseResult); + } + + protected override Expression VisitColumn(ColumnExpression columnExpression) + { + Check.NotNull(columnExpression, nameof(columnExpression)); + + IsNullable = !NonNullableColumns.Contains(columnExpression) && columnExpression.IsNullable; + + return columnExpression; + } + + protected override Expression VisitCrossApply(CrossApplyExpression crossApplyExpression) + { + Check.NotNull(crossApplyExpression, nameof(crossApplyExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var table = (TableExpressionBase)Visit(crossApplyExpression.Table); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return crossApplyExpression.Update(table); + } + + protected override Expression VisitCrossJoin(CrossJoinExpression crossJoinExpression) + { + Check.NotNull(crossJoinExpression, nameof(crossJoinExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var table = (TableExpressionBase)Visit(crossJoinExpression.Table); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return crossJoinExpression.Update(table); + } + + protected override Expression VisitExcept(ExceptExpression exceptExpression) + { + Check.NotNull(exceptExpression, nameof(exceptExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var source1 = (SelectExpression)Visit(exceptExpression.Source1); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + var source2 = (SelectExpression)Visit(exceptExpression.Source2); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return exceptExpression.Update(source1, source2); + } + + protected override Expression VisitExists(ExistsExpression existsExpression) + { + Check.NotNull(existsExpression, nameof(existsExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var newSubquery = (SelectExpression)Visit(existsExpression.Subquery); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return existsExpression.Update(newSubquery); + } + + protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) + { + Check.NotNull(fromSqlExpression, nameof(fromSqlExpression)); + + return fromSqlExpression; + } + + protected override Expression VisitIn(InExpression inExpression) + { + Check.NotNull(inExpression, nameof(inExpression)); + + var canOptimize = CanOptimize; + CanOptimize = false; + IsNullable = false; + var item = (SqlExpression)Visit(inExpression.Item); + var itemNullable = IsNullable; + IsNullable = false; + + if (inExpression.Subquery != null) + { + var subquery = (SelectExpression)Visit(inExpression.Subquery); + IsNullable |= itemNullable; + CanOptimize = canOptimize; + + return inExpression.Update(item, values: null, subquery); + } + + // for relational null semantics just leave as is + // same for values we don't know how to properly handle (i.e. other than constant or parameter) + if (UseRelationalNulls + || !(inExpression.Values is SqlConstantExpression || inExpression.Values is SqlParameterExpression)) + { + var values = (SqlExpression)Visit(inExpression.Values); + IsNullable |= itemNullable; + CanOptimize = canOptimize; + + return inExpression.Update(item, values, subquery: null); + } + + // for c# null semantics we need to remove nulls from Values and add IsNull/IsNotNull when necessary + var (inValues, hasNullValue) = ProcessInExpressionValues(inExpression.Values); + + CanOptimize = canOptimize; + + // either values array is empty or only contains null + if (((List)inValues.Value).Count == 0) + { + IsNullable = false; + + // a IN () -> false + // non_nullable IN (NULL) -> false + // a NOT IN () -> true + // non_nullable NOT IN (NULL) -> true + // nullable IN (NULL) -> nullable IS NULL + // nullable NOT IN (NULL) -> nullable IS NOT NULL + return !hasNullValue || !itemNullable + ? (SqlExpression)SqlExpressionFactory.Constant( + inExpression.IsNegated, + inExpression.TypeMapping) + : inExpression.IsNegated + ? SqlExpressionFactory.IsNotNull(item) + : SqlExpressionFactory.IsNull(item); + } + + if (!itemNullable + || (CanOptimize && !inExpression.IsNegated && !hasNullValue)) + { + IsNullable = itemNullable; + + // non_nullable IN (1, 2) -> non_nullable IN (1, 2) + // non_nullable IN (1, 2, NULL) -> non_nullable IN (1, 2) + // non_nullable NOT IN (1, 2) -> non_nullable NOT IN (1, 2) + // non_nullable NOT IN (1, 2, NULL) -> non_nullable NOT IN (1, 2) + // nullable IN (1, 2) -> nullable IN (1, 2) (optimized) + return inExpression.Update(item, inValues, subquery: null); + } + + // adding null comparison term to remove nulls completely from the resulting expression + IsNullable = false; + + // nullable IN (1, 2) -> nullable IN (1, 2) AND nullable IS NOT NULL (full) + // nullable IN (1, 2, NULL) -> nullable IN (1, 2) OR nullable IS NULL (full) + // nullable NOT IN (1, 2) -> nullable NOT IN (1, 2) OR nullable IS NULL (full) + // nullable NOT IN (1, 2, NULL) -> nullable NOT IN (1, 2) AND nullable IS NOT NULL (full) + return inExpression.IsNegated == hasNullValue + ? SqlExpressionFactory.AndAlso( + inExpression.Update(item, inValues, subquery: null), + SqlExpressionFactory.IsNotNull(item)) + : SqlExpressionFactory.OrElse( + inExpression.Update(item, inValues, subquery: null), + SqlExpressionFactory.IsNull(item)); + + (SqlConstantExpression processedValues, bool hasNullValue) ProcessInExpressionValues(SqlExpression valuesExpression) + { + var inValues = new List(); + var hasNullValue = false; + RelationalTypeMapping typeMapping = null; + + IEnumerable values = null; + if (valuesExpression is SqlConstantExpression sqlConstant) + { + typeMapping = sqlConstant.TypeMapping; + values = (IEnumerable)sqlConstant.Value; + } + + if (valuesExpression is SqlParameterExpression sqlParameter) + { + CanCache = false; + typeMapping = sqlParameter.TypeMapping; + values = (IEnumerable)ParameterValues[sqlParameter.Name]; + } + + foreach (var value in values) + { + if (value == null) + { + hasNullValue = true; + continue; + } + + inValues.Add(value); + } + + // this is only correct if constant values are the only things allowed here, i.e no mixing of constants and columns + var processedValues = (SqlConstantExpression)Visit(SqlExpressionFactory.Constant(inValues, typeMapping)); + + return (processedValues, hasNullValue); + } + } + + protected override Expression VisitInnerJoin(InnerJoinExpression innerJoinExpression) + { + Check.NotNull(innerJoinExpression, nameof(innerJoinExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var newTable = (TableExpressionBase)Visit(innerJoinExpression.Table); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + var newJoinPredicate = VisitJoinPredicate((SqlBinaryExpression)innerJoinExpression.JoinPredicate); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return newJoinPredicate is SqlConstantExpression constantJoinPredicate + && constantJoinPredicate.Value is bool boolPredicate + && boolPredicate + ? (Expression)new CrossJoinExpression(newTable) + : innerJoinExpression.Update(newTable, newJoinPredicate); + } + + protected override Expression VisitIntersect(IntersectExpression intersectExpression) + { + Check.NotNull(intersectExpression, nameof(intersectExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var source1 = (SelectExpression)Visit(intersectExpression.Source1); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + var source2 = (SelectExpression)Visit(intersectExpression.Source2); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return intersectExpression.Update(source1, source2); + } + + protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpression) + { + Check.NotNull(leftJoinExpression, nameof(leftJoinExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var newTable = (TableExpressionBase)Visit(leftJoinExpression.Table); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + var newJoinPredicate = VisitJoinPredicate((SqlBinaryExpression)leftJoinExpression.JoinPredicate); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return leftJoinExpression.Update(newTable, newJoinPredicate); + } + + private SqlExpression VisitJoinPredicate(SqlBinaryExpression predicate) + { + var canOptimize = CanOptimize; + CanOptimize = true; + + if (predicate.OperatorType == ExpressionType.Equal) + { + IsNullable = false; + var left = (SqlExpression)Visit(predicate.Left); + var leftNullable = IsNullable; + IsNullable = false; + var right = (SqlExpression)Visit(predicate.Right); + var rightNullable = IsNullable; + + var result = OptimizeComparison( + predicate.Update(left, right), + left, + right, + leftNullable, + rightNullable, + CanOptimize); + + CanOptimize = canOptimize; + + return result; + } + + if (predicate.OperatorType == ExpressionType.AndAlso) + { + var newPredicate = (SqlExpression)VisitSqlBinary(predicate); + CanOptimize = canOptimize; + + return newPredicate; + } + + throw new InvalidOperationException("Unexpected join predicate shape: " + predicate); + } + + protected override Expression VisitLike(LikeExpression likeExpression) + { + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + IsNullable = false; + var newMatch = (SqlExpression)Visit(likeExpression.Match); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + var isNullable = IsNullable; + IsNullable = false; + var newPattern = (SqlExpression)Visit(likeExpression.Pattern); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + isNullable |= IsNullable; + IsNullable = false; + var newEscapeChar = (SqlExpression)Visit(likeExpression.EscapeChar); + IsNullable |= isNullable; + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return likeExpression.Update(newMatch, newPattern, newEscapeChar); + } + + protected override Expression VisitOrdering(OrderingExpression orderingExpression) + { + Check.NotNull(orderingExpression, nameof(orderingExpression)); + + var expression = (SqlExpression)Visit(orderingExpression.Expression); + + return orderingExpression.Update(expression); + } + + protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExpression) + { + Check.NotNull(outerApplyExpression, nameof(outerApplyExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var table = (TableExpressionBase)Visit(outerApplyExpression.Table); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return outerApplyExpression.Update(table); + } + + protected override Expression VisitProjection(ProjectionExpression projectionExpression) + { + Check.NotNull(projectionExpression, nameof(projectionExpression)); + + var expression = (SqlExpression)Visit(projectionExpression.Expression); + + return projectionExpression.Update(expression); + } + + protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression) + { + Check.NotNull(rowNumberExpression, nameof(rowNumberExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var changed = false; + var partitions = new List(); + foreach (var partition in rowNumberExpression.Partitions) + { + var newPartition = (SqlExpression)Visit(partition); + changed |= newPartition != partition; + partitions.Add(newPartition); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + var orderings = new List(); + foreach (var ordering in rowNumberExpression.Orderings) + { + var newOrdering = (OrderingExpression)Visit(ordering); + changed |= newOrdering != ordering; + orderings.Add(newOrdering); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + CanOptimize = canOptimize; + + return rowNumberExpression.Update(partitions, orderings); + } + + protected override Expression VisitScalarSubquery(ScalarSubqueryExpression scalarSubqueryExpression) + { + Check.NotNull(scalarSubqueryExpression, nameof(scalarSubqueryExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var subquery = (SelectExpression)Visit(scalarSubqueryExpression.Subquery); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return scalarSubqueryExpression.Update(subquery); + } + + protected override Expression VisitSelect(SelectExpression selectExpression) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + + var changed = false; + var canOptimize = CanOptimize; + var projections = new List(); + CanOptimize = false; + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + foreach (var item in selectExpression.Projection) + { + var updatedProjection = (ProjectionExpression)Visit(item); + projections.Add(updatedProjection); + changed |= updatedProjection != item; + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + var tables = new List(); + foreach (var table in selectExpression.Tables) + { + var newTable = (TableExpressionBase)Visit(table); + changed |= newTable != table; + tables.Add(newTable); + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + CanOptimize = true; + var predicate = (SqlExpression)Visit(selectExpression.Predicate); + changed |= predicate != selectExpression.Predicate; + + if (predicate is SqlConstantExpression predicateConstantExpression + && predicateConstantExpression.Value is bool predicateBoolValue + && predicateBoolValue) + { + predicate = null; + changed = true; + } + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + var groupBy = new List(); + CanOptimize = false; + foreach (var groupingKey in selectExpression.GroupBy) + { + var newGroupingKey = (SqlExpression)Visit(groupingKey); + changed |= newGroupingKey != groupingKey; + groupBy.Add(newGroupingKey); + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + CanOptimize = true; + var having = (SqlExpression)Visit(selectExpression.Having); + changed |= having != selectExpression.Having; + + if (having is SqlConstantExpression havingConstantExpression + && havingConstantExpression.Value is bool havingBoolValue + && havingBoolValue) + { + having = null; + changed = true; + } + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + var orderings = new List(); + CanOptimize = false; + foreach (var ordering in selectExpression.Orderings) + { + var orderingExpression = (SqlExpression)Visit(ordering.Expression); + changed |= orderingExpression != ordering.Expression; + orderings.Add(ordering.Update(orderingExpression)); + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + var offset = (SqlExpression)Visit(selectExpression.Offset); + changed |= offset != selectExpression.Offset; + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + var limit = (SqlExpression)Visit(selectExpression.Limit); + changed |= limit != selectExpression.Limit; + + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + CanOptimize = canOptimize; + + // we assume SelectExpression can always be null + // (e.g. projecting non-nullable column but with predicate that filters out all rows) + IsNullable = true; + + return changed + ? selectExpression.Update( + projections, tables, predicate, groupBy, having, orderings, limit, offset, selectExpression.IsDistinct, + selectExpression.Alias) + : selectExpression; + } + + protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression) + { + Check.NotNull(sqlBinaryExpression, nameof(sqlBinaryExpression)); + + IsNullable = false; + var canOptimize = CanOptimize; + + CanOptimize = CanOptimize && (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso + || sqlBinaryExpression.OperatorType == ExpressionType.OrElse); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var left = (SqlExpression)Visit(sqlBinaryExpression.Left); + var leftNullable = IsNullable; + var leftNonNullableColumns = NonNullableColumns.ToList(); + + IsNullable = false; + + if (sqlBinaryExpression.OperatorType != ExpressionType.AndAlso) + { + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + var right = (SqlExpression)Visit(sqlBinaryExpression.Right); + var rightNullable = IsNullable; + if (sqlBinaryExpression.OperatorType == ExpressionType.OrElse) + { + var intersect = leftNonNullableColumns.Intersect(NonNullableColumns).ToList(); + NonNullableColumns.Clear(); + NonNullableColumns.AddRange(intersect); + } + else if (sqlBinaryExpression.OperatorType != ExpressionType.AndAlso) + { + // in case of AndAlso we already have what we need as the column information propagates from left to right + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + if (sqlBinaryExpression.OperatorType == ExpressionType.Coalesce) + { + IsNullable = leftNullable && rightNullable; + CanOptimize = canOptimize; + + return sqlBinaryExpression.Update(left, right); + } + + // nullableStringColumn + NULL -> COALESCE(nullableStringColumn, "") + "" + if (sqlBinaryExpression.OperatorType == ExpressionType.Add + && sqlBinaryExpression.Type == typeof(string)) + { + if (leftNullable) + { + left = AddNullConcatenationProtection(left, sqlBinaryExpression.TypeMapping); + } + + if (rightNullable) + { + right = AddNullConcatenationProtection(right, sqlBinaryExpression.TypeMapping); + } + + return sqlBinaryExpression.Update(left, right); + } + + if (sqlBinaryExpression.OperatorType == ExpressionType.Equal + || sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) + { + var updated = sqlBinaryExpression.Update(left, right); + + var optimized = OptimizeComparison( + updated, + left, + right, + leftNullable, + rightNullable, + canOptimize); + + if (optimized is SqlUnaryExpression optimizedUnary + && optimizedUnary.OperatorType == ExpressionType.NotEqual + && optimizedUnary.Operand is ColumnExpression optimizedUnaryColumnOperand) + { + NonNullableColumns.Add(optimizedUnaryColumnOperand); + } + + // we assume that NullSemantics rewrite is only needed (on the current level) + // if the optimization didn't make any changes. + // Reason is that optimization can/will change the nullability of the resulting expression + // and that inforation is not tracked/stored anywhere + // so we can no longer rely on nullabilities that we computed earlier (leftNullable, rightNullable) + // when performing null semantics rewrite. + // It should be fine because current optimizations *radically* change the expression + // (e.g. binary -> unary, or binary -> constant) + // but we need to pay attention in the future if we introduce more subtle transformations here + if (optimized == updated + && (leftNullable || rightNullable) + && !UseRelationalNulls) + { + var rewriteNullSemanticsResult = RewriteNullSemantics( + updated, + updated.Left, + updated.Right, + leftNullable, + rightNullable, + canOptimize); + + CanOptimize = canOptimize; + + return rewriteNullSemanticsResult; + } + + CanOptimize = canOptimize; + + return optimized; + } + + IsNullable = leftNullable || rightNullable; + CanOptimize = canOptimize; + + var result = sqlBinaryExpression.Update(left, right); + + return result is SqlBinaryExpression sqlBinaryResult + && (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso || sqlBinaryExpression.OperatorType == ExpressionType.OrElse) + ? SimplifyLogicalSqlBinaryExpression(sqlBinaryResult) + : result; + + SqlExpression AddNullConcatenationProtection(SqlExpression argument, RelationalTypeMapping typeMapping) + => argument switch + { + SqlConstantExpression _ => SqlExpressionFactory.Constant(string.Empty, typeMapping), + SqlParameterExpression _ => SqlExpressionFactory.Constant(string.Empty, typeMapping), + ColumnExpression _ => SqlExpressionFactory.Coalesce(argument, SqlExpressionFactory.Constant(string.Empty, typeMapping)), + _ => argument + }; + } + + private SqlExpression OptimizeComparison( + SqlBinaryExpression sqlBinaryExpression, + SqlExpression left, + SqlExpression right, + bool leftNullable, + bool rightNullable, + bool canOptimize) + { + var leftNullValue = leftNullable && (left is SqlConstantExpression || left is SqlParameterExpression); + var rightNullValue = rightNullable && (right is SqlConstantExpression || right is SqlParameterExpression); + + // a == null -> a IS NULL + // a != null -> a IS NOT NULL + if (rightNullValue) + { + var result = sqlBinaryExpression.OperatorType == ExpressionType.Equal + ? ProcessNullNotNull(SqlExpressionFactory.IsNull(left), leftNullable) + : ProcessNullNotNull(SqlExpressionFactory.IsNotNull(left), leftNullable); + + IsNullable = false; + CanOptimize = canOptimize; + + return result; + } + + // null == a -> a IS NULL + // null != a -> a IS NOT NULL + if (leftNullValue) + { + var result = sqlBinaryExpression.OperatorType == ExpressionType.Equal + ? ProcessNullNotNull(SqlExpressionFactory.IsNull(right), rightNullable) + : ProcessNullNotNull(SqlExpressionFactory.IsNotNull(right), rightNullable); + + IsNullable = false; + CanOptimize = canOptimize; + + return result; + } + + if (IsTrueOrFalse(right) is bool rightTrueFalseValue + && !leftNullable) + { + IsNullable = leftNullable; + CanOptimize = canOptimize; + + // only correct in 2-value logic + // a == true -> a + // a == false -> !a + // a != true -> !a + // a != false -> a + return sqlBinaryExpression.OperatorType == ExpressionType.Equal ^ rightTrueFalseValue + ? OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(left)) + : left; + } + + if (IsTrueOrFalse(left) is bool leftTrueFalseValue + && !rightNullable) + { + IsNullable = rightNullable; + CanOptimize = canOptimize; + + // only correct in 2-value logic + // true == a -> a + // false == a -> !a + // true != a -> !a + // false != a -> a + return sqlBinaryExpression.OperatorType == ExpressionType.Equal ^ leftTrueFalseValue + ? SqlExpressionFactory.Not(right) + : right; + } + + // only correct in 2-value logic + // a == a -> true + // a != a -> false + if (!leftNullable + && left.Equals(right)) + { + IsNullable = false; + CanOptimize = canOptimize; + + return SqlExpressionFactory.Constant( + sqlBinaryExpression.OperatorType == ExpressionType.Equal, + sqlBinaryExpression.TypeMapping); + } + + if (!leftNullable + && !rightNullable + && (sqlBinaryExpression.OperatorType == ExpressionType.Equal || sqlBinaryExpression.OperatorType == ExpressionType.NotEqual)) + { + var leftUnary = left as SqlUnaryExpression; + var rightUnary = right as SqlUnaryExpression; + + var leftNegated = leftUnary?.IsLogicalNot() == true; + var rightNegated = rightUnary?.IsLogicalNot() == true; + + if (leftNegated) + { + left = leftUnary.Operand; + } + + if (rightNegated) + { + right = rightUnary.Operand; + } + + // a == b <=> !a == !b -> a == b + // !a == b <=> a == !b -> a != b + // a != b <=> !a != !b -> a != b + // !a != b <=> a != !b -> a == b + return sqlBinaryExpression.OperatorType == ExpressionType.Equal ^ leftNegated == rightNegated + ? SqlExpressionFactory.NotEqual(left, right) + : SqlExpressionFactory.Equal(left, right); + } + + return sqlBinaryExpression.Update(left, right); + + bool? IsTrueOrFalse(SqlExpression sqlExpression) + { + if (sqlExpression is SqlConstantExpression sqlConstantExpression && sqlConstantExpression.Value is bool boolConstant) + { + return boolConstant; + } + + return null; + } + } + + private SqlExpression RewriteNullSemantics( + SqlBinaryExpression sqlBinaryExpression, + SqlExpression left, + SqlExpression right, + bool leftNullable, + bool rightNullable, + bool canOptimize) + { + var leftUnary = left as SqlUnaryExpression; + var rightUnary = right as SqlUnaryExpression; + + var leftNegated = leftUnary?.IsLogicalNot() == true; + var rightNegated = rightUnary?.IsLogicalNot() == true; + + if (leftNegated) + { + left = leftUnary.Operand; + } + + if (rightNegated) + { + right = rightUnary.Operand; + } + + var leftIsNull = ProcessNullNotNull(SqlExpressionFactory.IsNull(left), leftNullable); + var leftIsNotNull = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(leftIsNull)); + + var rightIsNull = ProcessNullNotNull(SqlExpressionFactory.IsNull(right), rightNullable); + var rightIsNotNull = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(rightIsNull)); + + // optimized expansion which doesn't distinguish between null and false + if (canOptimize + && sqlBinaryExpression.OperatorType == ExpressionType.Equal + && !leftNegated + && !rightNegated) + { + // when we use optimized form, the result can still be nullable + if (leftNullable && rightNullable) + { + IsNullable = true; + CanOptimize = canOptimize; + + return SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.Equal(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)))); + } + + if ((leftNullable && !rightNullable) + || (!leftNullable && rightNullable)) + { + IsNullable = true; + CanOptimize = canOptimize; + + return SqlExpressionFactory.Equal(left, right); + } + } + + // doing a full null semantics rewrite - removing all nulls from truth table + IsNullable = false; + CanOptimize = canOptimize; + + if (sqlBinaryExpression.OperatorType == ExpressionType.Equal) + { + if (leftNullable && rightNullable) + { + // ?a == ?b <=> !(?a) == !(?b) -> [(a == b) && (a != null && b != null)] || (a == null && b == null)) + // !(?a) == ?b <=> ?a == !(?b) -> [(a != b) && (a != null && b != null)] || (a == null && b == null) + return leftNegated == rightNegated + ? ExpandNullableEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull) + : ExpandNegatedNullableEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull); + } + + if (leftNullable && !rightNullable) + { + // ?a == b <=> !(?a) == !b -> (a == b) && (a != null) + // !(?a) == b <=> ?a == !b -> (a != b) && (a != null) + return leftNegated == rightNegated + ? ExpandNullableEqualNonNullable(left, right, leftIsNotNull) + : ExpandNegatedNullableEqualNonNullable(left, right, leftIsNotNull); + } + + if (rightNullable && !leftNullable) + { + // a == ?b <=> !a == !(?b) -> (a == b) && (b != null) + // !a == ?b <=> a == !(?b) -> (a != b) && (b != null) + return leftNegated == rightNegated + ? ExpandNullableEqualNonNullable(left, right, rightIsNotNull) + : ExpandNegatedNullableEqualNonNullable(left, right, rightIsNotNull); + } + } + + if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) + { + if (leftNullable && rightNullable) + { + // ?a != ?b <=> !(?a) != !(?b) -> [(a != b) || (a == null || b == null)] && (a != null || b != null) + // !(?a) != ?b <=> ?a != !(?b) -> [(a == b) || (a == null || b == null)] && (a != null || b != null) + return leftNegated == rightNegated + ? ExpandNullableNotEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull) + : ExpandNegatedNullableNotEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull); + } + + if (leftNullable) + { + // ?a != b <=> !(?a) != !b -> (a != b) || (a == null) + // !(?a) != b <=> ?a != !b -> (a == b) || (a == null) + return leftNegated == rightNegated + ? ExpandNullableNotEqualNonNullable(left, right, leftIsNull) + : ExpandNegatedNullableNotEqualNonNullable(left, right, leftIsNull); + } + + if (rightNullable) + { + // a != ?b <=> !a != !(?b) -> (a != b) || (b == null) + // !a != ?b <=> a != !(?b) -> (a == b) || (b == null) + return leftNegated == rightNegated + ? ExpandNullableNotEqualNonNullable(left, right, rightIsNull) + : ExpandNegatedNullableNotEqualNonNullable(left, right, rightIsNull); + } + } + + return sqlBinaryExpression.Update(left, right); + } + + private SqlExpression SimplifyLogicalSqlBinaryExpression( + SqlBinaryExpression sqlBinaryExpression) + { + var leftUnary = sqlBinaryExpression.Left as SqlUnaryExpression; + var rightUnary = sqlBinaryExpression.Right as SqlUnaryExpression; + if (leftUnary != null + && rightUnary != null + && (leftUnary.OperatorType == ExpressionType.Equal || leftUnary.OperatorType == ExpressionType.NotEqual) + && (rightUnary.OperatorType == ExpressionType.Equal || rightUnary.OperatorType == ExpressionType.NotEqual) + && leftUnary.Operand.Equals(rightUnary.Operand)) + { + // a is null || a is null -> a is null + // a is not null || a is not null -> a is not null + // a is null && a is null -> a is null + // a is not null && a is not null -> a is not null + // a is null || a is not null -> true + // a is null && a is not null -> false + return leftUnary.OperatorType == rightUnary.OperatorType + ? (SqlExpression)leftUnary + : SqlExpressionFactory.Constant(sqlBinaryExpression.OperatorType == ExpressionType.OrElse, sqlBinaryExpression.TypeMapping); + } + + // true && a -> a + // true || a -> true + // false && a -> false + // false || a -> a + if (sqlBinaryExpression.Left is SqlConstantExpression newLeftConstant) + { + return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso + ? (bool)newLeftConstant.Value + ? sqlBinaryExpression.Right + : newLeftConstant + : (bool)newLeftConstant.Value + ? newLeftConstant + : sqlBinaryExpression.Right; + } + else if (sqlBinaryExpression.Right is SqlConstantExpression newRightConstant) + { + // a && true -> a + // a || true -> true + // a && false -> false + // a || false -> a + return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso + ? (bool)newRightConstant.Value + ? sqlBinaryExpression.Left + : newRightConstant + : (bool)newRightConstant.Value + ? newRightConstant + : sqlBinaryExpression.Left; + } + + return sqlBinaryExpression; + } + + protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) + { + Check.NotNull(sqlConstantExpression, nameof(sqlConstantExpression)); + + IsNullable = sqlConstantExpression.Value == null; + + return sqlConstantExpression; + } + + protected override Expression VisitSqlFragment(SqlFragmentExpression sqlFragmentExpression) + { + Check.NotNull(sqlFragmentExpression, nameof(sqlFragmentExpression)); + + return sqlFragmentExpression; + } + + protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression) + { + Check.NotNull(sqlFunctionExpression, nameof(sqlFunctionExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + + var newInstance = (SqlExpression)Visit(sqlFunctionExpression.Instance); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + var newArguments = new SqlExpression[sqlFunctionExpression.Arguments.Count]; + for (var i = 0; i < newArguments.Length; i++) + { + newArguments[i] = (SqlExpression)Visit(sqlFunctionExpression.Arguments[i]); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + CanOptimize = canOptimize; + + // TODO: #18555 + IsNullable = true; + + return sqlFunctionExpression.Update(newInstance, newArguments); + } + + protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) + { + Check.NotNull(sqlParameterExpression, nameof(sqlParameterExpression)); + + IsNullable = ParameterValues[sqlParameterExpression.Name] == null; + + return IsNullable + ? SqlExpressionFactory.Constant(null, sqlParameterExpression.TypeMapping) + : (SqlExpression)sqlParameterExpression; + } + + protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression) + { + Check.NotNull(sqlUnaryExpression, nameof(sqlUnaryExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + IsNullable = false; + var canOptimize = CanOptimize; + CanOptimize = false; + + var operand = (SqlExpression)Visit(sqlUnaryExpression.Operand); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + CanOptimize = canOptimize; + var updated = sqlUnaryExpression.Update(operand); + + if (sqlUnaryExpression.OperatorType == ExpressionType.Equal + || sqlUnaryExpression.OperatorType == ExpressionType.NotEqual) + { + // result of IsNull/IsNotNull can never be null + var isNullable = IsNullable; + IsNullable = false; + + return ProcessNullNotNull(updated, isNullable); + } + + return !IsNullable && sqlUnaryExpression.OperatorType == ExpressionType.Not + ? OptimizeNonNullableNotExpression(updated) + : updated; + } + + private SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression) + { + if (sqlUnaryExpression.OperatorType != ExpressionType.Not) + { + return sqlUnaryExpression; + } + + switch (sqlUnaryExpression.Operand) + { + // !(true) -> false + // !(false) -> true + case SqlConstantExpression constantOperand + when constantOperand.Value is bool value: + { + return SqlExpressionFactory.Constant(!value, sqlUnaryExpression.TypeMapping); + } + + case InExpression inOperand: + return inOperand.Negate(); + + case SqlUnaryExpression sqlUnaryOperand: + { + switch (sqlUnaryOperand.OperatorType) + { + // !(!a) -> a + case ExpressionType.Not: + return sqlUnaryOperand.Operand; + + //!(a IS NULL) -> a IS NOT NULL + case ExpressionType.Equal: + return SqlExpressionFactory.IsNotNull(sqlUnaryOperand.Operand); + + //!(a IS NOT NULL) -> a IS NULL + case ExpressionType.NotEqual: + return SqlExpressionFactory.IsNull(sqlUnaryOperand.Operand); + } + break; + } + + case SqlBinaryExpression sqlBinaryOperand: + { + // optimizations below are only correct in 2-value logic + // De Morgan's + if (sqlBinaryOperand.OperatorType == ExpressionType.AndAlso + || sqlBinaryOperand.OperatorType == ExpressionType.OrElse) + { + // since entire AndAlso/OrElse expression is non-nullable, both sides of it (left and right) must also be non-nullable + // so it's safe to perform recursive optimization here + var left = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(sqlBinaryOperand.Left)); + var right = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(sqlBinaryOperand.Right)); + + return SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.MakeBinary( + sqlBinaryOperand.OperatorType == ExpressionType.AndAlso + ? ExpressionType.OrElse + : ExpressionType.AndAlso, + left, + right, + sqlBinaryOperand.TypeMapping)); + } + + // !(a == b) -> a != b + // !(a != b) -> a == b + // !(a > b) -> a <= b + // !(a >= b) -> a < b + // !(a < b) -> a >= b + // !(a <= b) -> a > b + if (TryNegate(sqlBinaryOperand.OperatorType, out var negated)) + { + return SqlExpressionFactory.MakeBinary( + negated, + sqlBinaryOperand.Left, + sqlBinaryOperand.Right, + sqlBinaryOperand.TypeMapping); + } + } + break; + } + + //var sqlUnaryOperand = sqlUnaryExpression.Operand as SqlUnaryExpression; + //if (sqlUnaryExpression != null) + //{ + // switch (sqlUnaryOperand.OperatorType) + // { + // // !(!a) -> a + // case ExpressionType.Not: + // return sqlUnaryOperand.Operand; + + // //!(a IS NULL) -> a IS NOT NULL + // case ExpressionType.Equal: + // return SqlExpressionFactory.IsNotNull(sqlUnaryOperand.Operand); + + // //!(a IS NOT NULL) -> a IS NULL + // case ExpressionType.NotEqual: + // return SqlExpressionFactory.IsNull(sqlUnaryOperand.Operand); + // } + //} + + //var sqlBinaryOperand = sqlUnaryExpression.Operand as SqlBinaryExpression; + //if (sqlBinaryOperand == null) + //{ + // return sqlUnaryExpression; + //} + + //// optimizations below are only correct in 2-value logic + //// De Morgan's + //if (sqlBinaryOperand.OperatorType == ExpressionType.AndAlso + // || sqlBinaryOperand.OperatorType == ExpressionType.OrElse) + //{ + // // since entire AndAlso/OrElse expression is non-nullable, both sides of it (left and right) must also be non-nullable + // // so it's safe to perform recursive optimization here + // var left = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(sqlBinaryOperand.Left)); + // var right = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(sqlBinaryOperand.Right)); + + // return SimplifyLogicalSqlBinaryExpression( + // SqlExpressionFactory.MakeBinary( + // sqlBinaryOperand.OperatorType == ExpressionType.AndAlso + // ? ExpressionType.OrElse + // : ExpressionType.AndAlso, + // left, + // right, + // sqlBinaryOperand.TypeMapping)); + //} + + //// !(a == b) -> a != b + //// !(a != b) -> a == b + //// !(a > b) -> a <= b + //// !(a >= b) -> a < b + //// !(a < b) -> a >= b + //// !(a <= b) -> a > b + //if (TryNegate(sqlBinaryOperand.OperatorType, out var negated)) + //{ + // return SqlExpressionFactory.MakeBinary( + // negated, + // sqlBinaryOperand.Left, + // sqlBinaryOperand.Right, + // sqlBinaryOperand.TypeMapping); + //} + + return sqlUnaryExpression; + + static bool TryNegate(ExpressionType expressionType, out ExpressionType result) + { + var negated = expressionType switch + { + ExpressionType.Equal => ExpressionType.NotEqual, + ExpressionType.NotEqual => ExpressionType.Equal, + ExpressionType.GreaterThan => ExpressionType.LessThanOrEqual, + ExpressionType.GreaterThanOrEqual => ExpressionType.LessThan, + ExpressionType.LessThan => ExpressionType.GreaterThanOrEqual, + ExpressionType.LessThanOrEqual => ExpressionType.GreaterThan, + _ => (ExpressionType?)null + }; + + result = negated ?? default; + + return negated.HasValue; + } + } + + protected virtual SqlExpression ProcessNullNotNull( + [NotNull] SqlUnaryExpression sqlUnaryExpression, + bool? operandNullable) + { + Check.NotNull(sqlUnaryExpression, nameof(sqlUnaryExpression)); + + if (operandNullable == false) + { + // when we know that operand is non-nullable: + // not_null_operand is null-> false + // not_null_operand is not null -> true + return SqlExpressionFactory.Constant( + sqlUnaryExpression.OperatorType == ExpressionType.NotEqual, + sqlUnaryExpression.TypeMapping); + } + + switch (sqlUnaryExpression.Operand) + { + case SqlConstantExpression sqlConstantOperand: + // null_value_constant is null -> true + // null_value_constant is not null -> false + // not_null_value_constant is null -> false + // not_null_value_constant is not null -> true + return SqlExpressionFactory.Constant( + sqlConstantOperand.Value == null ^ sqlUnaryExpression.OperatorType == ExpressionType.NotEqual, + sqlUnaryExpression.TypeMapping); + + case SqlParameterExpression sqlParameterOperand: + // null_value_parameter is null -> true + // null_value_parameter is not null -> false + // not_null_value_parameter is null -> false + // not_null_value_parameter is not null -> true + return SqlExpressionFactory.Constant( + ParameterValues[sqlParameterOperand.Name] == null ^ sqlUnaryExpression.OperatorType == ExpressionType.NotEqual, + sqlUnaryExpression.TypeMapping); + + case ColumnExpression columnOperand + when !columnOperand.IsNullable || NonNullableColumns.Contains(columnOperand): + { + // IsNull(non_nullable_column) -> false + // IsNotNull(non_nullable_column) -> true + return SqlExpressionFactory.Constant( + sqlUnaryExpression.OperatorType == ExpressionType.NotEqual, + sqlUnaryExpression.TypeMapping); + } + + case SqlUnaryExpression sqlUnaryOperand: + switch (sqlUnaryOperand.OperatorType) + { + case ExpressionType.Convert: + case ExpressionType.Not: + case ExpressionType.Negate: + // op(a) is null -> a is null + // op(a) is not null -> a is not null + return ProcessNullNotNull( + SqlExpressionFactory.MakeUnary( + sqlUnaryExpression.OperatorType, + sqlUnaryOperand.Operand, + sqlUnaryExpression.Type, + sqlUnaryExpression.TypeMapping), + operandNullable); + + case ExpressionType.Equal: + case ExpressionType.NotEqual: + // (a is null) is null -> false + // (a is not null) is null -> false + // (a is null) is not null -> true + // (a is not null) is not null -> true + return SqlExpressionFactory.Constant( + sqlUnaryOperand.OperatorType == ExpressionType.NotEqual, + sqlUnaryOperand.TypeMapping); + } + break; + + case SqlBinaryExpression sqlBinaryOperand + when sqlBinaryOperand.OperatorType != ExpressionType.AndAlso + && sqlBinaryOperand.OperatorType != ExpressionType.OrElse: + { + // in general: + // binaryOp(a, b) == null -> a == null || b == null + // binaryOp(a, b) != null -> a != null && b != null + // for coalesce: + // (a ?? b) == null -> a == null && b == null + // (a ?? b) != null -> a != null || b != null + // for AndAlso, OrElse we can't do this optimization + // we could do something like this, but it seems too complicated: + // (a && b) == null -> a == null && b != 0 || a != 0 && b == null + // NOTE: we don't preserve nullabilities of left/right individually so we are using nullability binary expression as a whole + // this may lead to missing some optimizations, where one of the operands (left or right) is not nullable and the other one is + var left = ProcessNullNotNull( + SqlExpressionFactory.MakeUnary( + sqlUnaryExpression.OperatorType, + sqlBinaryOperand.Left, + typeof(bool), + sqlUnaryExpression.TypeMapping), + operandNullable: null); + + var right = ProcessNullNotNull( + SqlExpressionFactory.MakeUnary( + sqlUnaryExpression.OperatorType, + sqlBinaryOperand.Right, + typeof(bool), + sqlUnaryExpression.TypeMapping), + operandNullable: null); + + return sqlBinaryOperand.OperatorType == ExpressionType.Coalesce + ? SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.MakeBinary( + sqlUnaryExpression.OperatorType == ExpressionType.Equal + ? ExpressionType.AndAlso + : ExpressionType.OrElse, + left, + right, + sqlUnaryExpression.TypeMapping)) + : SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.MakeBinary( + sqlUnaryExpression.OperatorType == ExpressionType.Equal + ? ExpressionType.OrElse + : ExpressionType.AndAlso, + left, + right, + sqlUnaryExpression.TypeMapping)); + } + } + + return sqlUnaryExpression; + } + + protected override Expression VisitTable(TableExpression tableExpression) + { + Check.NotNull(tableExpression, nameof(tableExpression)); + + return tableExpression; + } + + protected override Expression VisitUnion(UnionExpression unionExpression) + { + Check.NotNull(unionExpression, nameof(unionExpression)); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var canOptimize = CanOptimize; + CanOptimize = false; + var source1 = (SelectExpression)Visit(unionExpression.Source1); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + var source2 = (SelectExpression)Visit(unionExpression.Source2); + CanOptimize = canOptimize; + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + + return unionExpression.Update(source1, source2); + } + + private List FindNonNullableColumns(SqlExpression sqlExpression) + { + var result = new List(); + if (sqlExpression is SqlBinaryExpression sqlBinaryExpression) + { + if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) + { + if (sqlBinaryExpression.Left is ColumnExpression leftColumn + && leftColumn.IsNullable + && sqlBinaryExpression.Right is SqlConstantExpression rightConstant + && rightConstant.Value == null) + { + result.Add(leftColumn); + } + + if (sqlBinaryExpression.Right is ColumnExpression rightColumn + && rightColumn.IsNullable + && sqlBinaryExpression.Left is SqlConstantExpression leftConstant + && leftConstant.Value == null) + { + result.Add(rightColumn); + } + } + + if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso) + { + result.AddRange(FindNonNullableColumns(sqlBinaryExpression.Left)); + result.AddRange(FindNonNullableColumns(sqlBinaryExpression.Right)); + } + } + + return result; + } + + // ?a == ?b -> [(a == b) && (a != null && b != null)] || (a == null && b == null)) + // + // a | b | F1 = a == b | F2 = (a != null && b != null) | F3 = F1 && F2 | + // | | | | | + // 0 | 0 | 1 | 1 | 1 | + // 0 | 1 | 0 | 1 | 0 | + // 0 | N | N | 0 | 0 | + // 1 | 0 | 0 | 1 | 0 | + // 1 | 1 | 1 | 1 | 1 | + // 1 | N | N | 0 | 0 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + // N | N | N | 0 | 0 | + // + // a | b | F4 = (a == null && b == null) | Final = F3 OR F4 | + // | | | | + // 0 | 0 | 0 | 1 OR 0 = 1 | + // 0 | 1 | 0 | 0 OR 0 = 0 | + // 0 | N | 0 | 0 OR 0 = 0 | + // 1 | 0 | 0 | 0 OR 0 = 0 | + // 1 | 1 | 0 | 1 OR 0 = 1 | + // 1 | N | 0 | 0 OR 0 = 0 | + // N | 0 | 0 | 0 OR 0 = 0 | + // N | 1 | 0 | 0 OR 0 = 0 | + // N | N | 1 | 0 OR 1 = 1 | + private SqlExpression ExpandNullableEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.Equal(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + leftIsNotNull, + rightIsNotNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + leftIsNull, + rightIsNull)))); + + // !(?a) == ?b -> [(a != b) && (a != null && b != null)] || (a == null && b == null) + // + // a | b | F1 = a != b | F2 = (a != null && b != null) | F3 = F1 && F2 | + // | | | | | + // 0 | 0 | 0 | 1 | 0 | + // 0 | 1 | 1 | 1 | 1 | + // 0 | N | N | 0 | 0 | + // 1 | 0 | 1 | 1 | 1 | + // 1 | 1 | 0 | 1 | 0 | + // 1 | N | N | 0 | 0 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + // N | N | N | 0 | 0 | + // + // a | b | F4 = (a == null && b == null) | Final = F3 OR F4 | + // | | | | + // 0 | 0 | 0 | 0 OR 0 = 0 | + // 0 | 1 | 0 | 1 OR 0 = 1 | + // 0 | N | 0 | 0 OR 0 = 0 | + // 1 | 0 | 0 | 1 OR 0 = 1 | + // 1 | 1 | 0 | 0 OR 0 = 0 | + // 1 | N | 0 | 0 OR 0 = 0 | + // N | 0 | 0 | 0 OR 0 = 0 | + // N | 1 | 0 | 0 OR 0 = 0 | + // N | N | 1 | 0 OR 1 = 1 | + private SqlExpression ExpandNegatedNullableEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.NotEqual(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + leftIsNotNull, + rightIsNotNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + leftIsNull, + rightIsNull)))); + + // ?a == b -> (a == b) && (a != null) + // + // a | b | F1 = a == b | F2 = (a != null) | Final = F1 && F2 | + // | | | | | + // 0 | 0 | 1 | 1 | 1 | + // 0 | 1 | 0 | 1 | 0 | + // 1 | 0 | 0 | 1 | 0 | + // 1 | 1 | 1 | 1 | 1 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + private SqlExpression ExpandNullableEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.Equal(left, right), + leftIsNotNull)); + + // !(?a) == b -> (a != b) && (a != null) + // + // a | b | F1 = a != b | F2 = (a != null) | Final = F1 && F2 | + // | | | | | + // 0 | 0 | 0 | 1 | 0 | + // 0 | 1 | 1 | 1 | 1 | + // 1 | 0 | 1 | 1 | 1 | + // 1 | 1 | 0 | 1 | 0 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + private SqlExpression ExpandNegatedNullableEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.NotEqual(left, right), + leftIsNotNull)); + + // ?a != ?b -> [(a != b) || (a == null || b == null)] && (a != null || b != null) + // + // a | b | F1 = a != b | F2 = (a == null || b == null) | F3 = F1 || F2 | + // | | | | | + // 0 | 0 | 0 | 0 | 0 | + // 0 | 1 | 1 | 0 | 1 | + // 0 | N | N | 1 | 1 | + // 1 | 0 | 1 | 0 | 1 | + // 1 | 1 | 0 | 0 | 0 | + // 1 | N | N | 1 | 1 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + // N | N | N | 1 | 1 | + // + // a | b | F4 = (a != null || b != null) | Final = F3 && F4 | + // | | | | + // 0 | 0 | 1 | 0 && 1 = 0 | + // 0 | 1 | 1 | 1 && 1 = 1 | + // 0 | N | 1 | 1 && 1 = 1 | + // 1 | 0 | 1 | 1 && 1 = 1 | + // 1 | 1 | 1 | 0 && 1 = 0 | + // 1 | N | 1 | 1 && 1 = 1 | + // N | 0 | 1 | 1 && 1 = 1 | + // N | 1 | 1 | 1 && 1 = 1 | + // N | N | 0 | 1 && 0 = 0 | + //private SqlBinaryExpression ExpandNullableNotEqualNullable( + // SqlExpression left, + // SqlExpression right, + // SqlExpression leftIsNull, + // SqlExpression leftIsNotNull, + // SqlExpression rightIsNull, + // SqlExpression rightIsNotNull) + // => SqlExpressionFactory.AndAlso( + // SqlExpressionFactory.OrElse( + // SqlExpressionFactory.NotEqual(left, right), + // SqlExpressionFactory.OrElse( + // leftIsNull, + // rightIsNull)), + // SqlExpressionFactory.OrElse( + // leftIsNotNull, + // rightIsNotNull)); + + private SqlExpression ExpandNullableNotEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.NotEqual(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + leftIsNull, + rightIsNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + leftIsNotNull, + rightIsNotNull)))); + + // !(?a) != ?b -> [(a == b) || (a == null || b == null)] && (a != null || b != null) + // + // a | b | F1 = a == b | F2 = (a == null || b == null) | F3 = F1 || F2 | + // | | | | | + // 0 | 0 | 1 | 0 | 1 | + // 0 | 1 | 0 | 0 | 0 | + // 0 | N | N | 1 | 1 | + // 1 | 0 | 0 | 0 | 0 | + // 1 | 1 | 1 | 0 | 1 | + // 1 | N | N | 1 | 1 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + // N | N | N | 1 | 1 | + // + // a | b | F4 = (a != null || b != null) | Final = F3 && F4 | + // | | | | + // 0 | 0 | 1 | 1 && 1 = 1 | + // 0 | 1 | 1 | 0 && 1 = 0 | + // 0 | N | 1 | 1 && 1 = 1 | + // 1 | 0 | 1 | 0 && 1 = 0 | + // 1 | 1 | 1 | 1 && 1 = 1 | + // 1 | N | 1 | 1 && 1 = 1 | + // N | 0 | 1 | 1 && 1 = 1 | + // N | 1 | 1 | 1 && 1 = 1 | + // N | N | 0 | 1 && 0 = 0 | + private SqlExpression ExpandNegatedNullableNotEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.Equal(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + leftIsNull, + rightIsNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + leftIsNotNull, + rightIsNotNull)))); + + // ?a != b -> (a != b) || (a == null) + // + // a | b | F1 = a != b | F2 = (a == null) | Final = F1 OR F2 | + // | | | | | + // 0 | 0 | 0 | 0 | 0 | + // 0 | 1 | 1 | 0 | 1 | + // 1 | 0 | 1 | 0 | 1 | + // 1 | 1 | 0 | 0 | 0 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + private SqlExpression ExpandNullableNotEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.NotEqual(left, right), + leftIsNull)); + + // !(?a) != b -> (a == b) || (a == null) + // + // a | b | F1 = a == b | F2 = (a == null) | F3 = F1 OR F2 | + // | | | | | + // 0 | 0 | 1 | 0 | 1 | + // 0 | 1 | 0 | 0 | 0 | + // 1 | 0 | 0 | 0 | 0 | + // 1 | 1 | 1 | 0 | 1 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + private SqlExpression ExpandNegatedNullableNotEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.Equal(left, right), + leftIsNull)); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs index 4839f974e0c..e20e26715d4 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/InExpression.cs @@ -54,11 +54,11 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) { Check.NotNull(visitor, nameof(visitor)); - var newItem = (SqlExpression)visitor.Visit(Item); + var item = (SqlExpression)visitor.Visit(Item); var subquery = (SelectExpression)visitor.Visit(Subquery); var values = (SqlExpression)visitor.Visit(Values); - return Update(newItem, values, subquery); + return Update(item, values, subquery); } public virtual InExpression Negate() => new InExpression(Item, !IsNegated, Values, Subquery, TypeMapping); diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 685999152bc..c1c825b87a8 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -23,24 +23,24 @@ public SearchConditionConvertingExpressionVisitor( _sqlExpressionFactory = sqlExpressionFactory; } - private Expression ApplyConversion(SqlExpression sqlExpression, bool condition) + private SqlExpression ApplyConversion(SqlExpression sqlExpression, bool condition) => _isSearchCondition ? ConvertToSearchCondition(sqlExpression, condition) : ConvertToValue(sqlExpression, condition); - private Expression ConvertToSearchCondition(SqlExpression sqlExpression, bool condition) + private SqlExpression ConvertToSearchCondition(SqlExpression sqlExpression, bool condition) => condition ? sqlExpression : BuildCompareToExpression(sqlExpression); - private Expression ConvertToValue(SqlExpression sqlExpression, bool condition) + private SqlExpression ConvertToValue(SqlExpression sqlExpression, bool condition) { return condition ? _sqlExpressionFactory.Case( new[] { new CaseWhenClause( - sqlExpression, + SimplifyNegatedBinary(sqlExpression), _sqlExpressionFactory.ApplyDefaultTypeMapping(_sqlExpressionFactory.Constant(true))) }, _sqlExpressionFactory.Constant(false)) @@ -48,9 +48,33 @@ private Expression ConvertToValue(SqlExpression sqlExpression, bool condition) } private SqlExpression BuildCompareToExpression(SqlExpression sqlExpression) - { - return _sqlExpressionFactory.Equal(sqlExpression, _sqlExpressionFactory.Constant(true)); - } + => sqlExpression is SqlConstantExpression sqlConstantExpression + && sqlConstantExpression.Value is bool boolValue + ? _sqlExpressionFactory.Equal( + boolValue + ? _sqlExpressionFactory.Constant(1) + : _sqlExpressionFactory.Constant(0), + _sqlExpressionFactory.Constant(1)) + : _sqlExpressionFactory.Equal( + sqlExpression, + _sqlExpressionFactory.Constant(true)); + + // !(a == b) -> (a != b) + // !(a != b) -> (a == b) + private SqlExpression SimplifyNegatedBinary(SqlExpression sqlExpression) + => sqlExpression is SqlUnaryExpression sqlUnaryExpression + && sqlUnaryExpression.OperatorType == ExpressionType.Not + && (sqlUnaryExpression.Type == typeof(bool) || sqlUnaryExpression.Type == typeof(bool?)) + && sqlUnaryExpression.Operand is SqlBinaryExpression sqlBinaryOperand + && (sqlBinaryOperand.OperatorType == ExpressionType.Equal || sqlBinaryOperand.OperatorType == ExpressionType.NotEqual) + ? _sqlExpressionFactory.MakeBinary( + sqlBinaryOperand.OperatorType == ExpressionType.Equal + ? ExpressionType.NotEqual + : ExpressionType.Equal, + sqlBinaryOperand.Left, + sqlBinaryOperand.Right, + sqlBinaryOperand.TypeMapping) + : sqlExpression; protected override Expression VisitCase(CaseExpression caseExpression) { @@ -273,9 +297,13 @@ when sqlUnaryExpression.IsLogicalNot(): } var operand = (SqlExpression)Visit(sqlUnaryExpression.Operand); + _isSearchCondition = parentSearchCondition; - return ApplyConversion(sqlUnaryExpression.Update(operand), condition: resultCondition); + return SimplifyNegatedBinary( + ApplyConversion( + sqlUnaryExpression.Update(operand), + condition: resultCondition)); } protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs index b7cf63d1973..2fda9fae3db 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs @@ -27,8 +27,8 @@ public override (SelectExpression selectExpression, bool canCache) Optimize( var (optimizedSelectExpression, canCache) = base.Optimize(selectExpression, parametersValues); - var searchConditionOptimized = (SelectExpression)new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory) - .Visit(optimizedSelectExpression); + var searchConditionOptimized = (SelectExpression)new SearchConditionConvertingExpressionVisitor( + Dependencies.SqlExpressionFactory).Visit(optimizedSelectExpression); return (searchConditionOptimized, canCache); } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs index f29dd958ed4..7b60153578c 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryTranslationPostprocessor.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query; @@ -16,13 +15,5 @@ public SqlServerQueryTranslationPostprocessor( : base(dependencies, relationalDependencies, queryCompilationContext) { } - - public override Expression Process(Expression query) - { - query = base.Process(query); - query = new SearchConditionConvertingExpressionVisitor(SqlExpressionFactory).Visit(query); - - return query; - } } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs index 5c8b9ac34bd..adbfd52d806 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs @@ -492,6 +492,18 @@ public override Task GroupJoin_Subquery_with_Take_Then_SelectMany_Where(bool asy return base.GroupJoin_Subquery_with_Take_Then_SelectMany_Where(async); } + [ConditionalTheory(Skip = "Issue#17246")] + public override Task Inner_join_with_tautology_predicate_converts_to_cross_join(bool async) + { + return base.Inner_join_with_tautology_predicate_converts_to_cross_join(async); + } + + [ConditionalTheory(Skip = "Issue#17246")] + public override Task Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(bool async) + { + return base.Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs index 57f89a74ffa..e9bf5dca845 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs @@ -316,7 +316,7 @@ public virtual void Contains_with_local_array_closure_false_with_null() AssertQuery(es => es.Where(e => !ids.Contains(e.NullableStringA))); } - [ConditionalFact(Skip = "issue #14171")] + [ConditionalFact] public virtual void Contains_with_local_nullable_array_closure_negated() { string[] ids = { "Foo" }; @@ -946,40 +946,58 @@ join e2 in _clientData._entities2 } } - [ConditionalFact(Skip = "issue #14171")] + [ConditionalFact] public virtual void Null_semantics_contains() { - using var ctx = CreateContext(); var ids = new List { 1, 2 }; - var query1 = ctx.Entities1.Where(e => ids.Contains(e.NullableIntA)); - var result1 = query1.ToList(); + AssertQuery(es => es.Where(e => ids.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids.Contains(e.NullableIntA))); - var query2 = ctx.Entities1.Where(e => !ids.Contains(e.NullableIntA)); - var result2 = query2.ToList(); + var ids2 = new List { 1, 2, null }; + AssertQuery(es => es.Where(e => ids2.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids2.Contains(e.NullableIntA))); - var ids2 = new List - { - 1, - 2, - null - }; - var query3 = ctx.Entities1.Where(e => ids.Contains(e.NullableIntA)); - var result3 = query3.ToList(); + AssertQuery(es => es.Where(e => new List { 1, 2 }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List { 1, 2 }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => new List { 1, 2, null }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List { 1, 2, null }.Contains(e.NullableIntA))); + } - var query4 = ctx.Entities1.Where(e => !ids.Contains(e.NullableIntA)); - var result4 = query4.ToList(); + [ConditionalFact] + public virtual void Null_semantics_contains_array_with_no_values() + { + var ids = new List(); + AssertQuery(es => es.Where(e => ids.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids.Contains(e.NullableIntA))); - var query5 = ctx.Entities1.Where(e => !new List { 1, 2 }.Contains(e.NullableIntA)); - var result5 = query5.ToList(); + var ids2 = new List { null }; + AssertQuery(es => es.Where(e => ids2.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids2.Contains(e.NullableIntA))); - var query6 = ctx.Entities1.Where( - e => !new List - { - 1, - 2, - null - }.Contains(e.NullableIntA)); - var result6 = query6.ToList(); + AssertQuery(es => es.Where(e => new List().Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List().Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => new List { null }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List { null }.Contains(e.NullableIntA))); + } + + [ConditionalFact] + public virtual void Null_semantics_contains_non_nullable_argument() + { + var ids = new List { 1, 2, null }; + AssertQuery(es => es.Where(e => ids.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids.Contains(e.IntA))); + + var ids2 = new List { 1, 2, }; + AssertQuery(es => es.Where(e => ids2.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids2.Contains(e.IntA))); + + var ids3 = new List(); + AssertQuery(es => es.Where(e => ids3.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids3.Contains(e.IntA))); + + var ids4 = new List { null }; + AssertQuery(es => es.Where(e => ids4.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids4.Contains(e.IntA))); } [ConditionalFact] @@ -1021,6 +1039,23 @@ public virtual void Null_semantics_with_null_check_complex() var result3 = query3.ToList(); } + [ConditionalFact] + public virtual void Null_semantics_with_null_check_complex2() + { + using var ctx = CreateContext(); + var query1 = ctx.Entities1.Where(e => ((e.NullableBoolA != null) + && (e.NullableBoolB != null) + && ((e.NullableBoolB != e.NullableBoolA) || (e.NullableBoolC != null)) + && (e.NullableBoolC != e.NullableBoolB)) + || (e.NullableBoolC != e.BoolB)).ToList(); + + var query2 = ctx.Entities1.Where(e => ((e.NullableBoolA != null) + && (e.NullableBoolB != null) + && ((e.NullableBoolB != e.NullableBoolA) || (e.NullableBoolC != null)) + && (e.NullableBoolC != e.NullableBoolB)) + || (e.NullableBoolB != e.BoolB)).ToList(); + } + [ConditionalFact] public virtual void IsNull_on_complex_expression() { @@ -1044,6 +1079,110 @@ public virtual void Coalesce_not_equal() AssertQuery(es => es.Where(e => (e.NullableIntA ?? 0) != 0)); } + [ConditionalFact] + public virtual void Negated_order_comparison_on_non_nullable_arguments_gets_optimized() + { + var i = 1; + AssertQuery(es => es.Where(e => !(e.IntA > i))); + AssertQuery(es => es.Where(e => !(e.IntA >= i))); + AssertQuery(es => es.Where(e => !(e.IntA < i))); + AssertQuery(es => es.Where(e => !(e.IntA <= i))); + } + + [ConditionalFact(Skip = "issue #9544")] + public virtual void Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized() + { + var i = 1; + AssertQuery(es => es.Where(e => !(e.NullableIntA > i))); + AssertQuery(es => es.Where(e => !(e.NullableIntA >= i))); + AssertQuery(es => es.Where(e => !(e.NullableIntA < i))); + AssertQuery(es => es.Where(e => !(e.NullableIntA <= i))); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagates_inside_binary_AndAlso() + { + AssertQuery(es => es.Where(e => e.NullableStringA != null && e.NullableStringB != null && e.NullableStringA != e.NullableStringB)); + } + + [ConditionalFact] + public virtual void Nullable_column_info_doesnt_propagate_inside_binary_OrElse() + { + AssertQuery(es => es.Where(e => (e.NullableStringA != null || e.NullableStringB != null) && e.NullableStringA != e.NullableStringB)); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagates_inside_binary_OrElse_when_info_is_duplicated() + { + AssertQuery(es => es.Where(e => ((e.NullableStringA != null && e.NullableStringB != null) || (e.NullableStringA != null)) && e.NullableStringA != e.NullableStringB)); + AssertQuery(es => es.Where(e => ((e.NullableStringA != null && e.NullableStringB != null) || (e.NullableStringB != null && e.NullableStringA != null)) && e.NullableStringA != e.NullableStringB)); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagates_inside_conditional() + { + using var ctx = CreateContext(); + var query1 = ctx.Entities1.Select(e => e.NullableStringA != null ? e.NullableStringA != e.StringA : e.BoolA).ToList(); + } + + [ConditionalFact] + public virtual void Nullable_column_info_doesnt_propagate_between_projections() + { + using var ctx = CreateContext(); + var query = ctx.Entities1.Select(e => new { Foo = e.NullableStringA != null, Bar = e.NullableStringA != e.StringA }).ToList(); + } + + [ConditionalFact] + public virtual void Nullable_column_info_doesnt_propagate_between_different_parts_of_select() + { + AssertQuery(es => from e1 in es + join e2 in es on e1.NullableBoolA != null equals false + where e1.NullableBoolA != e2.NullableBoolB + select e1); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagation_complex() + { + AssertQuery(es => es.Where(e => (e.NullableStringA != null && e.NullableBoolB != null && e.NullableStringC != null) + && ((e.NullableStringA != null || e.NullableBoolC != null) + && e.NullableBoolB != e.NullableBoolC))); + } + + [ConditionalFact] + public virtual void String_concat_with_both_arguments_being_null() + { + var prm = default(string); + + using var ctx = CreateContext(); + var prmPrmQuery = ctx.Entities1.Select(x => prm + prm).ToList(); + var prmPrmExpected = ctx.Entities1.ToList().Select(x => prm + prm).ToList(); + + var prmConstQuery = ctx.Entities1.Select(x => prm + null).ToList(); + var prmConstExpected = ctx.Entities1.ToList().Select(x => prm + null).ToList(); + + var prmColumn = ctx.Entities1.Select(x => prm + x.NullableStringA).ToList(); + var prmColumnExpected = ctx.Entities1.ToList().Select(x => prm + x.NullableStringA).ToList(); + + var constPrmQuery = ctx.Entities1.Select(x => null + prm).ToList(); + var constPrmExpected = ctx.Entities1.ToList().Select(x => null + prm).ToList(); + + var constConstQuery = ctx.Entities1.Select(x => (string)null + null).ToList(); + var constConstExpected = ctx.Entities1.ToList().Select(x => (string)null + null).ToList(); + + var constColumn = ctx.Entities1.Select(x => null + x.NullableStringA).ToList(); + var constColumnExpected = ctx.Entities1.ToList().Select(x => null + x.NullableStringA).ToList(); + + var columnPrmQuery = ctx.Entities1.Select(x => x.NullableStringB + prm).ToList(); + var columnPrmExpected = ctx.Entities1.ToList().Select(x => x.NullableStringB + prm).ToList(); + + var columnConstQuery = ctx.Entities1.Select(x => x.NullableStringB + null).ToList(); + var columnConstExpected = ctx.Entities1.ToList().Select(x => x.NullableStringB + null).ToList(); + + var columnColumn = ctx.Entities1.Select(x => x.NullableStringB + x.NullableStringA).ToList(); + var columnColumnExpected = ctx.Entities1.ToList().Select(x => x.NullableStringB + x.NullableStringA).ToList(); + } + protected static TResult Maybe(object caller, Func expression) where TResult : class { diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 8aec7e573ae..09f4c09d9dc 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -4567,7 +4567,7 @@ public virtual Task Nav_rewrite_doesnt_apply_null_protection_for_function_argume .Select(l1 => Math.Max(l1.OneToOne_Optional_PK1.Level1_Required_Id, 7))); } - [ConditionalTheory(Skip = "See issue#11464")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Accessing_optional_property_inside_result_operator_subquery(bool async) { diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index bfe6dcbe9a9..4730b31ba92 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -7168,7 +7168,7 @@ public virtual Task Where_null_parameter_is_not_null(bool async) ss => ss.Set().Where(g => false)); } - [ConditionalTheory(Skip = "issue #19019")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task OrderBy_StartsWith_with_null_parameter_as_argument(bool async) { @@ -7181,6 +7181,18 @@ public virtual Task OrderBy_StartsWith_with_null_parameter_as_argument(bool asyn assertOrder: true); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task OrderBy_Contains_empty_list(bool async) + { + var ids = new List(); + + return AssertQuery( + async, + ss => ss.Set().OrderBy(g => ids.Contains(g.SquadId)).Select(g => g), + ss => ss.Set().OrderBy(g => ids.Contains(g.SquadId)).Select(g => g)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Where_with_enum_flags_parameter(bool async) diff --git a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs index d1cee909566..ad066890ee9 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs @@ -708,5 +708,28 @@ join o in ss.Set().OrderBy(o => o.OrderID).Take(100) on c.CustomerID equa from o in lo.Where(x => x.CustomerID.StartsWith("A")) select new { c.CustomerID, o.OrderID }); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inner_join_with_tautology_predicate_converts_to_cross_join(bool async) + { + return AssertQuery( + async, + ss => from c in ss.Set().OrderBy(c => c.CustomerID).Take(10) + join o in ss.Set().OrderBy(o => o.OrderID).Take(10) on 1 equals 1 + select new { c.CustomerID, o.OrderID }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(bool async) + { + return AssertQuery( + async, + ss => from c in ss.Set().OrderBy(c => c.CustomerID).Take(10) + join o in ss.Set().OrderBy(o => o.OrderID).Take(10) on c.CustomerID != null equals true into grouping + from o in grouping.DefaultIfEmpty() + select new { c.CustomerID, o.OrderID }); + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs index fb2e41e13ab..fd115b48b52 100644 --- a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs @@ -538,7 +538,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK(EntityState state, bool async) @@ -548,7 +548,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_collection_not_found(EntityState state, bool async) @@ -1153,7 +1153,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_alternate_key(EntityState state, bool async) @@ -1163,7 +1163,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_collection_shadow_fk(EntityState state, bool async) @@ -1283,7 +1283,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(EntityState state, bool async) @@ -1293,7 +1293,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_collection_composite_key(EntityState state, bool async) @@ -1421,7 +1421,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_composite_key(EntityState state, bool async) @@ -1431,7 +1431,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 525853ef2e3..f5f029079fc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -326,7 +326,7 @@ public override async Task Method_call_on_optional_navigation_translates_to_null @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id] FROM [LevelOne] AS [l] LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id] -WHERE ([l0].[Name] = N'') OR ([l0].[Name] IS NOT NULL AND ([l0].[Name] IS NOT NULL AND (LEFT([l0].[Name], LEN([l0].[Name])) = [l0].[Name])))"); +WHERE ([l0].[Name] = N'') OR ([l0].[Name] IS NOT NULL AND (LEFT([l0].[Name], LEN([l0].[Name])) = [l0].[Name]))"); } public override async Task Optional_navigation_inside_method_call_translated_to_join_keeps_original_nullability(bool async) @@ -3305,9 +3305,10 @@ public override async Task Accessing_optional_property_inside_result_operator_su await base.Accessing_optional_property_inside_result_operator_subquery(async); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_Inverse1Id], [l1].[OneToMany_Required_Self_Inverse1Id], [l1].[OneToOne_Optional_Self1Id], [l1.OneToOne_Optional_FK1].[Name] -FROM [LevelOne] AS [l1] -LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK1] ON [l1].[Id] = [l1.OneToOne_Optional_FK1].[Level1_Optional_Id]"); + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id] +FROM [LevelOne] AS [l] +LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id] +WHERE [l0].[Name] NOT IN (N'Name1', N'Name2') OR [l0].[Name] IS NULL"); } public override async Task Include_after_SelectMany_and_reference_navigation(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs index 28611e2633e..7cd2674ad6f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs @@ -45,15 +45,15 @@ WHERE CHARINDEX(N'_Ba_', [f].[FirstName]) > 0", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(N'%B%a%r', [f].[FirstName]) <= 0", +WHERE NOT (CHARINDEX(N'%B%a%r', [f].[FirstName]) > 0)", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(NULL, [f].[FirstName]) <= 0"); +WHERE NOT (CHARINDEX(NULL, [f].[FirstName]) > 0)"); } public override async Task String_contains_on_argument_with_wildcard_parameter(bool async) @@ -73,11 +73,9 @@ SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] WHERE (@__prm2_0 = N'') OR (CHARINDEX(@__prm2_0, [f].[FirstName]) > 0)", // - @"@__prm3_0=NULL (Size = 4000) - -SELECT [f].[FirstName] + @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(@__prm3_0, [f].[FirstName]) > 0", +WHERE CHARINDEX(NULL, [f].[FirstName]) > 0", // @"@__prm4_0='' (Size = 4000) @@ -95,19 +93,17 @@ FROM [FunkyCustomers] AS [f] SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE (@__prm6_0 <> N'') AND (CHARINDEX(@__prm6_0, [f].[FirstName]) <= 0)", +WHERE NOT ((@__prm6_0 = N'') OR (CHARINDEX(@__prm6_0, [f].[FirstName]) > 0))", // @"@__prm7_0='' (Size = 4000) SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE (@__prm7_0 <> N'') AND (CHARINDEX(@__prm7_0, [f].[FirstName]) <= 0)", +WHERE NOT ((@__prm7_0 = N'') OR (CHARINDEX(@__prm7_0, [f].[FirstName]) > 0))", // - @"@__prm8_0=NULL (Size = 4000) - -SELECT [f].[FirstName] + @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(@__prm8_0, [f].[FirstName]) <= 0"); +WHERE NOT (CHARINDEX(NULL, [f].[FirstName]) > 0)"); } public override async Task String_contains_on_argument_with_wildcard_column(bool async) @@ -129,7 +125,7 @@ public override async Task String_contains_on_argument_with_wildcard_column_nega @"SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] FROM [FunkyCustomers] AS [f] CROSS JOIN [FunkyCustomers] AS [f0] -WHERE (([f0].[LastName] <> N'') OR [f0].[LastName] IS NULL) AND (CHARINDEX([f0].[LastName], [f].[FirstName]) <= 0)"); +WHERE NOT ((([f0].[LastName] = N'') AND [f0].[LastName] IS NOT NULL) OR (CHARINDEX([f0].[LastName], [f].[FirstName]) > 0))"); } public override async Task String_starts_with_on_argument_with_wildcard_constant(bool async) @@ -147,7 +143,7 @@ WHERE [f].[FirstName] IS NOT NULL AND ([f].[FirstName] LIKE N'a\_%' ESCAPE N'\') // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f]", @@ -162,11 +158,11 @@ WHERE [f].[FirstName] IS NOT NULL AND NOT ([f].[FirstName] LIKE N'\%B\%a\%r%' ES // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_starts_with_on_argument_with_wildcard_parameter(bool async) @@ -188,7 +184,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"@__prm4_0='' (Size = 4000) @@ -216,7 +212,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_starts_with_on_argument_with_bracket(bool async) @@ -296,7 +292,7 @@ WHERE [f].[FirstName] IS NOT NULL AND ([f].[FirstName] LIKE N'%a\_' ESCAPE N'\') // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f]", @@ -311,11 +307,11 @@ WHERE [f].[FirstName] IS NOT NULL AND NOT ([f].[FirstName] LIKE N'%\%B\%a\%r' ES // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_ends_with_on_argument_with_wildcard_parameter(bool async) @@ -337,7 +333,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"@__prm4_0='' (Size = 4000) @@ -365,7 +361,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_ends_with_on_argument_with_wildcard_column(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 24d0f6fec0f..714b70d82b3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -28,7 +28,7 @@ public override async Task Entity_equality_empty(bool 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 CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Include_multiple_one_to_one_and_one_to_many(bool async) @@ -490,11 +490,9 @@ public override async Task Where_bitwise_and_nullable_enum_with_nullable_paramet FROM [Weapons] AS [w] WHERE ([w].[AmmunitionType] & @__ammunitionType_0) > 0", // - @"@__ammunitionType_0=NULL (DbType = Int32) - -SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] + @"SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] -WHERE ([w].[AmmunitionType] & @__ammunitionType_0) > 0"); +WHERE ([w].[AmmunitionType] & NULL) > 0"); } public override async Task Where_bitwise_or_enum(bool async) @@ -758,9 +756,7 @@ public override async Task Select_null_parameter(bool async) SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] FROM [Weapons] AS [w]", // - @"@__ammunitionType_0=NULL (DbType = Int32) - -SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] + @"SELECT [w].[Id], NULL AS [AmmoType] FROM [Weapons] AS [w]", // @"@__ammunitionType_0='2' (Nullable = true) @@ -768,9 +764,7 @@ public override async Task Select_null_parameter(bool async) SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] FROM [Weapons] AS [w]", // - @"@__ammunitionType_0=NULL (DbType = Int32) - -SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] + @"SELECT [w].[Id], NULL AS [AmmoType] FROM [Weapons] AS [w]"); } @@ -869,10 +863,7 @@ public override async Task Null_propagation_optimization1(bool 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 [g].[Discriminator] IN (N'Gear', N'Officer') AND (CASE - WHEN ([g].[LeaderNickname] = N'Marcus') AND [g].[LeaderNickname] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END = CAST(1 AS bit))"); +WHERE [g].[Discriminator] IN (N'Gear', N'Officer') AND (([g].[LeaderNickname] = N'Marcus') AND [g].[LeaderNickname] IS NOT NULL)"); } public override async Task Null_propagation_optimization2(bool async) @@ -1077,10 +1068,7 @@ public override async Task Select_null_propagation_negative7(bool async) AssertSql( @"SELECT CASE - WHEN [g].[LeaderNickname] IS NOT NULL THEN CASE - WHEN (([g].[LeaderNickname] = [g].[LeaderNickname]) AND [g].[LeaderNickname] IS NOT NULL) OR [g].[LeaderNickname] IS NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) - END + WHEN [g].[LeaderNickname] IS NOT NULL THEN CAST(1 AS bit) ELSE NULL END FROM [Gears] AS [g] @@ -1224,7 +1212,7 @@ public override async Task Where_compare_anonymous_types_with_uncorrelated_membe AssertSql( @"SELECT [g].[Nickname] FROM [Gears] AS [g] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Select_Where_Navigation_Scalar_Equals_Navigation_Scalar(bool async) @@ -6256,7 +6244,6 @@ ELSE NULL END IS NOT NULL THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); - } public override async Task GetValueOrDefault_in_projection(bool async) @@ -6668,7 +6655,7 @@ LEFT JOIN ( 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].[Discriminator] IN (N'Gear', N'Officer') -) AS [t0] ON ((([t].[GearNickName] = [t0].[Nickname]) AND ([t].[GearSquadId] = [t0].[SquadId])) AND [t].[Note] IS NOT NULL) AND [t].[Note] IS NOT NULL +) AS [t0] ON (([t].[GearNickName] = [t0].[Nickname]) AND ([t].[GearSquadId] = [t0].[SquadId])) AND [t].[Note] IS NOT NULL ORDER BY [t].[Id], [t0].[Nickname], [t0].[SquadId]"); } @@ -7071,7 +7058,7 @@ public override async Task Group_by_with_having_StartsWith_with_null_parameter_a FROM [Gears] AS [g] WHERE [g].[Discriminator] IN (N'Gear', N'Officer') GROUP BY [g].[FullName] -HAVING CAST(0 AS bit) = CAST(1 AS bit)"); +HAVING 0 = 1"); } public override async Task Select_StartsWith_with_null_parameter_as_argument(bool async) @@ -7079,10 +7066,7 @@ public override async Task Select_StartsWith_with_null_parameter_as_argument(boo await base.Select_StartsWith_with_null_parameter_as_argument(async); AssertSql( - @"SELECT CASE - WHEN CAST(0 AS bit) = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END + @"SELECT CAST(0 AS bit) FROM [Gears] AS [g] WHERE [g].[Discriminator] IN (N'Gear', N'Officer')"); } @@ -7116,7 +7100,20 @@ public override async Task OrderBy_StartsWith_with_null_parameter_as_argument(bo await base.OrderBy_StartsWith_with_null_parameter_as_argument(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 [g].[Discriminator] IN (N'Gear', N'Officer') +ORDER BY [g].[Nickname]"); + } + + public override async Task OrderBy_Contains_empty_list(bool async) + { + await base.OrderBy_Contains_empty_list(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 [g].[Discriminator] IN (N'Gear', N'Officer')"); } public override async Task Where_with_enum_flags_parameter(bool async) @@ -7142,7 +7139,7 @@ WHERE [g].[Discriminator] IN (N'Gear', N'Officer') AND (([g].[Rank] | @__rank_0) // @"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 CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task FirstOrDefault_navigation_access_entity_equality_in_where_predicate_apply_peneding_selector(bool async) @@ -7357,10 +7354,7 @@ public override async Task Conditional_expression_with_test_being_simplified_to_ FROM [Gears] AS [g] WHERE [g].[Discriminator] IN (N'Gear', N'Officer') AND (CASE WHEN [g].[HasSoulPatch] = @__prm_0 THEN CAST(1 AS bit) - ELSE CASE - WHEN CAST(0 AS bit) = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) - END + ELSE CAST(0 AS bit) END = CAST(1 AS bit))"); } @@ -7385,10 +7379,7 @@ FROM [Weapons] AS [w] WHERE [w].[Id] = [g].[SquadId]) IS NOT NULL THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END - ELSE CASE - WHEN CAST(0 AS bit) = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) - END + ELSE CAST(0 AS bit) END = CAST(1 AS bit))"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs index dd0f818fce9..84b1ad1de0b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs @@ -497,7 +497,7 @@ FROM [Animal] AS [a] FROM [Animal] AS [a0] WHERE [a0].[Discriminator] = N'Eagle' ) AS [t] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override void Member_access_on_intermediate_type_works() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index 71fdcd5636e..3dee9123006 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -821,7 +821,7 @@ public override async Task Contains_with_local_list_closure_all_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 CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Contains_with_local_list_inline(bool async) @@ -942,7 +942,7 @@ public override async Task Contains_with_local_collection_empty_closure(bool asy 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 CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Contains_with_local_collection_empty_inline(bool async) @@ -1171,10 +1171,8 @@ public override void Contains_over_entityType_with_null_should_rewrite_to_identi base.Contains_over_entityType_with_null_should_rewrite_to_identity_equality(); AssertSql( - @"@__entity_equality_p_0_OrderID=NULL (DbType = Int32) - -SELECT CASE - WHEN @__entity_equality_p_0_OrderID IN ( + @"SELECT CASE + WHEN NULL IN ( SELECT [o].[OrderID] FROM [Orders] AS [o] WHERE [o].[CustomerID] = N'VINET' diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 7fd2e5eddc1..6969acb44eb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -39,7 +39,7 @@ public override async Task String_StartsWith_Identity(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 ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_StartsWith_Column(bool async) @@ -49,7 +49,7 @@ public override async Task String_StartsWith_Column(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 ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_StartsWith_MethodCall(bool async) @@ -79,7 +79,7 @@ public override async Task String_EndsWith_Identity(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 ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_EndsWith_Column(bool async) @@ -89,7 +89,7 @@ public override async Task String_EndsWith_Column(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 ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_EndsWith_MethodCall(bool async) @@ -1182,7 +1182,7 @@ public override async Task Indexof_with_emptystring(bool async) AssertSql( @"SELECT CASE - WHEN N'' = N'' THEN 0 + WHEN 1 = 1 THEN 0 ELSE CAST(CHARINDEX(N'', [c].[ContactName]) AS int) - 1 END FROM [Customers] AS [c] @@ -1412,7 +1412,7 @@ public override async Task Static_equals_int_compared_to_long(bool async) AssertSql( @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Projecting_Math_Truncate_and_ordering_by_it_twice(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 7114bb61aa8..b3f448957c8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -1675,7 +1675,7 @@ WHEN NOT EXISTS ( SELECT 1 FROM [Orders] AS [o] GROUP BY [o].[CustomerID] - HAVING CAST(0 AS bit) = CAST(1 AS bit)) THEN CAST(1 AS bit) + HAVING 0 = 1) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } @@ -1690,7 +1690,7 @@ WHEN NOT EXISTS ( SELECT 1 FROM [Orders] AS [o] GROUP BY [o].[CustomerID] - HAVING SUM([o].[OrderID]) < 0) THEN CAST(1 AS bit) + HAVING NOT (SUM([o].[OrderID]) >= 0)) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs index bffae36883f..4dd10e801c0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs @@ -209,11 +209,11 @@ public override async Task Join_complex_condition(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -INNER JOIN ( +CROSS JOIN ( SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] WHERE [o].[OrderID] < 10250 -) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit) +) AS [t] WHERE [c].[CustomerID] = N'ALFKI'"); } @@ -469,6 +469,48 @@ WHERE [t].[CustomerID] IS NOT NULL AND ([t].[CustomerID] LIKE N'A%') ) AS [t0] ON [c].[CustomerID] = [t0].[CustomerID]"); } + public override async Task Inner_join_with_tautology_predicate_converts_to_cross_join(bool async) + { + await base.Inner_join_with_tautology_predicate_converts_to_cross_join(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t0].[OrderID] +FROM ( + SELECT TOP(@__p_0) [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] + ORDER BY [c].[CustomerID] +) AS [t] +CROSS JOIN ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[OrderID] +) AS [t0] +ORDER BY [t].[CustomerID]"); + } + + public override async Task Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(bool async) + { + await base.Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t0].[OrderID] +FROM ( + SELECT TOP(@__p_0) [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] + ORDER BY [c].[CustomerID] +) AS [t] +LEFT JOIN ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[OrderID] +) AS [t0] ON 1 = 1 +ORDER BY [t].[CustomerID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index 3e7d148376f..ce7f5c5b7cc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -234,7 +234,7 @@ public override async Task Entity_equality_null(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Entity_equality_null_composite_key(bool async) @@ -244,7 +244,7 @@ public override async Task Entity_equality_null_composite_key(bool async) AssertSql( @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] FROM [Order Details] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Entity_equality_not_null(bool async) @@ -307,7 +307,7 @@ public override async Task Entity_equality_through_include(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Entity_equality_orderby(bool async) @@ -1460,7 +1460,7 @@ public override async Task All_top_level_column(bool async) WHEN NOT EXISTS ( SELECT 1 FROM [Customers] AS [c] - WHERE (([c].[ContactName] <> N'') OR [c].[ContactName] IS NULL) AND ([c].[ContactName] IS NULL OR ([c].[ContactName] IS NULL OR ((LEFT([c].[ContactName], LEN([c].[ContactName])) <> [c].[ContactName]) OR LEFT([c].[ContactName], LEN([c].[ContactName])) IS NULL)))) THEN CAST(1 AS bit) + WHERE (([c].[ContactName] <> N'') OR [c].[ContactName] IS NULL) AND ([c].[ContactName] IS NULL OR ((LEFT([c].[ContactName], LEN([c].[ContactName])) <> [c].[ContactName]) OR LEFT([c].[ContactName], LEN([c].[ContactName])) IS NULL))) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } @@ -3035,7 +3035,7 @@ FROM [Orders] AS [o] // @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Parameter_extraction_short_circuits_3(bool async) @@ -4258,7 +4258,7 @@ public override async Task Comparing_different_entity_types_using_Equals(bool as @"SELECT [c].[CustomerID] FROM [Customers] AS [c] CROSS JOIN [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_entity_to_null_using_Equals(bool async) @@ -4308,7 +4308,7 @@ public override async Task Comparing_non_matching_entities_using_Equals(bool asy @"SELECT [c].[CustomerID] AS [Id1], [o].[OrderID] AS [Id2] FROM [Customers] AS [c] CROSS JOIN [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_non_matching_collection_navigations_using_Equals(bool async) @@ -4319,7 +4319,7 @@ public override async Task Comparing_non_matching_collection_navigations_using_E @"SELECT [c].[CustomerID] AS [Id1], [o].[OrderID] AS [Id2] FROM [Customers] AS [c] CROSS JOIN [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_collection_navigation_to_null(bool async) @@ -4329,7 +4329,7 @@ public override async Task Comparing_collection_navigation_to_null(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_collection_navigation_to_null_complex(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index ff6b7e4c1d0..db9ed1c6203 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -1168,7 +1168,7 @@ public override async Task Select_with_complex_expression_that_can_be_funcletize AssertSql( @"SELECT CASE - WHEN N'' = N'' THEN 0 + WHEN 1 = 1 THEN 0 ELSE CAST(CHARINDEX(N'', [c].[ContactName]) AS int) - 1 END FROM [Customers] AS [c] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index 6efa8b4e010..7e9a5004530 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -522,7 +522,7 @@ public override async Task Where_equals_using_object_overload_on_mismatched_type AssertSql( @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); // See issue#17498 //Assert.Contains( @@ -549,11 +549,11 @@ public override async Task Where_equals_on_mismatched_types_nullable_int_long(bo AssertSql( @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); // See issue#17498 //Assert.Contains( @@ -572,11 +572,11 @@ public override async Task Where_equals_on_mismatched_types_nullable_long_nullab AssertSql( @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); // See issue#17498 //Assert.Contains( @@ -898,7 +898,7 @@ public override async Task Where_constant_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 CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_is_not_null(bool async) @@ -918,7 +918,7 @@ public override async Task Where_null_is_not_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 CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_constant_is_not_null(bool async) @@ -1121,10 +1121,10 @@ public override async Task Where_negated_boolean_expression_compared_to_another_ @"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] FROM [Products] AS [p] WHERE CASE - WHEN [p].[ProductID] > 50 THEN CAST(1 AS bit) + WHEN [p].[ProductID] <= 50 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END = CASE - WHEN [p].[ProductID] > 20 THEN CAST(1 AS bit) + WHEN [p].[ProductID] <= 20 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } @@ -1253,7 +1253,7 @@ public override async Task Where_false(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 CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_default(bool async) @@ -1343,7 +1343,7 @@ public override async Task Where_concat_string_string_comparison(bool async) SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE (COALESCE(@__i_0, N'') + [c].[CustomerID]) = [c].[CompanyName]"); +WHERE (@__i_0 + [c].[CustomerID]) = [c].[CompanyName]"); } public override async Task Where_string_concat_method_comparison(bool async) @@ -1355,7 +1355,7 @@ public override async Task Where_string_concat_method_comparison(bool async) SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE (COALESCE(@__i_0, N'') + [c].[CustomerID]) = [c].[CompanyName]"); +WHERE (@__i_0 + [c].[CustomerID]) = [c].[CompanyName]"); } public override async Task Where_ternary_boolean_condition_true(bool async) @@ -1407,7 +1407,7 @@ public override async Task Where_ternary_boolean_condition_with_false_as_result_ AssertSql( @"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] FROM [Products] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_compare_constructed_equal(bool async) @@ -1665,7 +1665,7 @@ public override async Task Where_is_conditional(bool async) @"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] FROM [Products] AS [p] WHERE CASE - WHEN CAST(1 AS bit) = CAST(1 AS bit) THEN CAST(0 AS bit) + WHEN 1 = 1 THEN CAST(0 AS bit) ELSE CAST(1 AS bit) END = CAST(1 AS bit)"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs index 6b4f413188f..5bcac2619e2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs @@ -730,7 +730,9 @@ public override void Contains_with_local_nullable_array_closure_negated() base.Contains_with_local_nullable_array_closure_negated(); AssertSql( - @""); + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableStringA] NOT IN (N'Foo') OR [e].[NullableStringA] IS NULL"); } public override void Contains_with_local_array_closure_with_multiple_nulls() @@ -782,7 +784,7 @@ public override void Where_multiple_ands_with_nullable_parameter_and_constant() SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE (((([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL) AND [e].[NullableStringA] IS NOT NULL) AND [e].[NullableStringA] IS NOT NULL) AND (([e].[NullableStringA] <> @__prm3_2) OR [e].[NullableStringA] IS NULL)"); +WHERE ((([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL) AND [e].[NullableStringA] IS NOT NULL) AND ([e].[NullableStringA] <> @__prm3_2)"); } public override void Where_multiple_ands_with_nullable_parameter_and_constant_not_optimized() @@ -794,7 +796,7 @@ public override void Where_multiple_ands_with_nullable_parameter_and_constant_no SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE ((([e].[NullableStringB] IS NOT NULL AND (([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL)) AND [e].[NullableStringA] IS NOT NULL) AND [e].[NullableStringA] IS NOT NULL) AND (([e].[NullableStringA] <> @__prm3_2) OR [e].[NullableStringA] IS NULL)"); +WHERE (([e].[NullableStringB] IS NOT NULL AND (([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL)) AND [e].[NullableStringA] IS NOT NULL) AND ([e].[NullableStringA] <> @__prm3_2)"); } public override void Where_coalesce() @@ -1460,7 +1462,109 @@ public override void Null_semantics_contains() base.Null_semantics_contains(); AssertSql( - @""); + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) AND [e].[NullableIntA] IS NOT NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) AND [e].[NullableIntA] IS NOT NULL"); + } + + public override void Null_semantics_contains_array_with_no_values() + { + base.Null_semantics_contains_array_with_no_values(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NOT NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NOT NULL"); + } + + public override void Null_semantics_contains_non_nullable_argument() + { + base.Null_semantics_contains_non_nullable_argument(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] NOT IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] NOT IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]"); } public override void Null_semantics_with_null_check_simple() @@ -1507,6 +1611,20 @@ FROM [Entities1] AS [e] WHERE ([e].[NullableIntA] IS NOT NULL OR [e].[NullableIntB] IS NOT NULL) AND (([e].[NullableIntA] = [e].[NullableIntC]) OR ([e].[NullableIntA] IS NULL AND [e].[NullableIntC] IS NULL))"); } + public override void Null_semantics_with_null_check_complex2() + { + base.Null_semantics_with_null_check_complex2(); + + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE (([e].[NullableBoolA] IS NOT NULL AND ([e].[NullableBoolB] IS NOT NULL AND (([e].[NullableBoolB] <> [e].[NullableBoolA]) OR [e].[NullableBoolC] IS NOT NULL))) AND (([e].[NullableBoolC] <> [e].[NullableBoolB]) OR [e].[NullableBoolC] IS NULL)) OR (([e].[NullableBoolC] <> [e].[BoolB]) OR [e].[NullableBoolC] IS NULL)", + // + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE (([e].[NullableBoolA] IS NOT NULL AND ([e].[NullableBoolB] IS NOT NULL AND (([e].[NullableBoolB] <> [e].[NullableBoolA]) OR [e].[NullableBoolC] IS NOT NULL))) AND (([e].[NullableBoolC] <> [e].[NullableBoolB]) OR [e].[NullableBoolC] IS NULL)) OR (([e].[NullableBoolB] <> [e].[BoolB]) OR [e].[NullableBoolB] IS NULL)"); + } + public override void IsNull_on_complex_expression() { base.IsNull_on_complex_expression(); @@ -1539,6 +1657,129 @@ FROM [Entities1] AS [e] WHERE COALESCE([e].[NullableIntA], 0) <> 0"); } + public override void Negated_order_comparison_on_non_nullable_arguments_gets_optimized() + { + base.Negated_order_comparison_on_non_nullable_arguments_gets_optimized(); + + AssertSql( + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] <= @__i_0", + // + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] < @__i_0", + // + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] >= @__i_0", + // + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] > @__i_0"); + } + + public override void Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized() + { + base.Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized(); + + AssertSql( + @""); + } + + public override void Nullable_column_info_propagates_inside_binary_AndAlso() + { + base.Nullable_column_info_propagates_inside_binary_AndAlso(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE ([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL) AND ([e].[NullableStringA] <> [e].[NullableStringB])"); + } + + public override void Nullable_column_info_doesnt_propagate_inside_binary_OrElse() + { + base.Nullable_column_info_doesnt_propagate_inside_binary_OrElse(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringB] IS NOT NULL) AND ((([e].[NullableStringA] <> [e].[NullableStringB]) OR ([e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL)) AND ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringB] IS NOT NULL))"); + } + + public override void Nullable_column_info_propagates_inside_binary_OrElse_when_info_is_duplicated() + { + base.Nullable_column_info_propagates_inside_binary_OrElse_when_info_is_duplicated(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE (([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL) OR [e].[NullableStringA] IS NOT NULL) AND (([e].[NullableStringA] <> [e].[NullableStringB]) OR [e].[NullableStringB] IS NULL)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE (([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL) OR ([e].[NullableStringB] IS NOT NULL AND [e].[NullableStringA] IS NOT NULL)) AND ([e].[NullableStringA] <> [e].[NullableStringB])"); + } + + public override void Nullable_column_info_propagates_inside_conditional() + { + base.Nullable_column_info_propagates_inside_conditional(); + + AssertSql( + @"SELECT CASE + WHEN [e].[NullableStringA] IS NOT NULL THEN CASE + WHEN [e].[NullableStringA] <> [e].[StringA] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + ELSE [e].[BoolA] +END +FROM [Entities1] AS [e]"); + } + + public override void Nullable_column_info_doesnt_propagate_between_projections() + { + base.Nullable_column_info_doesnt_propagate_between_projections(); + + AssertSql( + @"SELECT CASE + WHEN [e].[NullableStringA] IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Foo], CASE + WHEN ([e].[NullableStringA] <> [e].[StringA]) OR [e].[NullableStringA] IS NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Bar] +FROM [Entities1] AS [e]"); + } + + public override void Nullable_column_info_doesnt_propagate_between_different_parts_of_select() + { + base.Nullable_column_info_doesnt_propagate_between_different_parts_of_select(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +INNER JOIN [Entities1] AS [e0] ON [e].[NullableBoolA] IS NULL +WHERE (([e].[NullableBoolA] <> [e0].[NullableBoolB]) OR ([e].[NullableBoolA] IS NULL OR [e0].[NullableBoolB] IS NULL)) AND ([e].[NullableBoolA] IS NOT NULL OR [e0].[NullableBoolB] IS NOT NULL)"); + } + + public override void Nullable_column_info_propagation_complex() + { + base.Nullable_column_info_propagation_complex(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE (([e].[NullableStringA] IS NOT NULL AND [e].[NullableBoolB] IS NOT NULL) AND [e].[NullableStringC] IS NOT NULL) AND (([e].[NullableBoolB] <> [e].[NullableBoolC]) OR [e].[NullableBoolC] IS NULL)"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index 68eef30073d..77c2bacfc2e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -97,7 +97,7 @@ FROM [OwnedPerson] AS [o15] WHERE [o13].[LeafAAddress_Country_PlanetId] IS NOT NULL ) AS [t14] ON [t11].[Id] = [t14].[Id] LEFT JOIN [Order] AS [o16] ON [o].[Id] = [o16].[ClientId] -WHERE CAST(0 AS bit) = CAST(1 AS bit) +WHERE 0 = 1 ORDER BY [o].[Id], [t].[Id], [o16].[ClientId], [o16].[Id]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs index 85e5315a9d6..95a6cc94a07 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs @@ -86,7 +86,7 @@ public override void DbContext_list_is_parameterized() AssertSql( @"SELECT [l].[Id], [l].[Tenant] FROM [ListFilter] AS [l] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [l].[Id], [l].[Tenant] FROM [ListFilter] AS [l] diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index 4b91cd4f9fa..440140585e2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -134,7 +134,7 @@ public override async Task String_StartsWith_Identity(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 (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND (""c"".""ContactName"" IS NOT NULL AND (((""c"".""ContactName"" LIKE ""c"".""ContactName"" || '%') AND (substr(""c"".""ContactName"", 1, length(""c"".""ContactName"")) = ""c"".""ContactName"")) OR (""c"".""ContactName"" = ''))))"); +WHERE (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND (((""c"".""ContactName"" LIKE ""c"".""ContactName"" || '%') AND (substr(""c"".""ContactName"", 1, length(""c"".""ContactName"")) = ""c"".""ContactName"")) OR (""c"".""ContactName"" = '')))"); } public override async Task String_StartsWith_Column(bool async) @@ -144,7 +144,7 @@ public override async Task String_StartsWith_Column(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 (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND (""c"".""ContactName"" IS NOT NULL AND (((""c"".""ContactName"" LIKE ""c"".""ContactName"" || '%') AND (substr(""c"".""ContactName"", 1, length(""c"".""ContactName"")) = ""c"".""ContactName"")) OR (""c"".""ContactName"" = ''))))"); +WHERE (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND (((""c"".""ContactName"" LIKE ""c"".""ContactName"" || '%') AND (substr(""c"".""ContactName"", 1, length(""c"".""ContactName"")) = ""c"".""ContactName"")) OR (""c"".""ContactName"" = '')))"); } public override async Task String_StartsWith_MethodCall(bool async) @@ -174,7 +174,7 @@ public override async Task String_EndsWith_Identity(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 (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND (""c"".""ContactName"" IS NOT NULL AND ((substr(""c"".""ContactName"", -length(""c"".""ContactName"")) = ""c"".""ContactName"") OR (""c"".""ContactName"" = ''))))"); +WHERE (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND ((substr(""c"".""ContactName"", -length(""c"".""ContactName"")) = ""c"".""ContactName"") OR (""c"".""ContactName"" = '')))"); } public override async Task String_EndsWith_Column(bool async) @@ -184,7 +184,7 @@ public override async Task String_EndsWith_Column(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 (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND (""c"".""ContactName"" IS NOT NULL AND ((substr(""c"".""ContactName"", -length(""c"".""ContactName"")) = ""c"".""ContactName"") OR (""c"".""ContactName"" = ''))))"); +WHERE (""c"".""ContactName"" = '') OR (""c"".""ContactName"" IS NOT NULL AND ((substr(""c"".""ContactName"", -length(""c"".""ContactName"")) = ""c"".""ContactName"") OR (""c"".""ContactName"" = '')))"); } public override async Task String_EndsWith_MethodCall(bool async)