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/NullabilityHandlingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/NullabilityHandlingExpressionVisitor.cs new file mode 100644 index 00000000000..25a5a915e28 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/NullabilityHandlingExpressionVisitor.cs @@ -0,0 +1,1474 @@ +// 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.Internal +{ + public class NullabilityHandlingExpressionVisitor : SqlExpressionVisitor + { + private readonly bool _useRelationalNulls; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IReadOnlyDictionary _parameterValues; + private readonly List _nonNullableColumns = new List(); + private bool _isNullable; + private bool _canOptimize; + + public virtual bool CanCache { get; set; } + + public NullabilityHandlingExpressionVisitor( + bool useRelationalNulls, + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + [NotNull] IReadOnlyDictionary parameterValues) + { + _useRelationalNulls = useRelationalNulls; + _sqlExpressionFactory = sqlExpressionFactory; + _parameterValues = parameterValues; + _canOptimize = true; + CanCache = true; + } + + private void RestoreNonNullableColumnsList(int counter) + { + _nonNullableColumns.RemoveRange(counter, _nonNullableColumns.Count - counter); + } + + 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 + if (_useRelationalNulls) + { + 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)); + } + + private (SqlConstantExpression processedValues, bool hasNullValue) ProcessInExpressionValues(SqlExpression valuesExpression) + { + var inValues = new List(); + var hasNullValue = false; + RelationalTypeMapping typeMapping = null; + + if (valuesExpression is SqlConstantExpression + || valuesExpression is SqlParameterExpression) + { + 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 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; + + 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 havingExpression = (SqlExpression)Visit(selectExpression.Having); + changed |= havingExpression != selectExpression.Having; + + 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, 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; + + _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); + } + + if (rightNullable) + { + right = AddNullConcatenationProtection(right); + } + + 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 result = RewriteNullSemantics( + updated, + updated.Left, + updated.Right, + leftNullable, + rightNullable, + canOptimize); + + _canOptimize = canOptimize; + + return result; + } + + _canOptimize = canOptimize; + + return optimized; + } + + _isNullable = leftNullable || rightNullable; + _canOptimize = canOptimize; + + return sqlBinaryExpression.Update(left, right); + + SqlExpression AddNullConcatenationProtection(SqlExpression argument) + => argument switch + { + SqlConstantExpression _ => _sqlExpressionFactory.Constant(string.Empty), + SqlParameterExpression _ => _sqlExpressionFactory.Constant(string.Empty), + ColumnExpression _ => _sqlExpressionFactory.Coalesce(argument, _sqlExpressionFactory.Constant(string.Empty)), + _ => 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 + ? _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 rightIsNull = ProcessNullNotNull(_sqlExpressionFactory.IsNull(right), rightNullable); + + // 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(left, right), + _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, rightIsNull) + : ExpandNegatedNullableEqualNullable(left, right, 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(left, right, leftIsNull) + : ExpandNegatedNullableEqualNonNullable(left, right, leftIsNull); + } + + 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, rightIsNull) + : ExpandNegatedNullableEqualNonNullable(left, right, rightIsNull); + } + } + + 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, rightIsNull) + : ExpandNegatedNullableNotEqualNullable(left, right, leftIsNull, rightIsNull); + } + + 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); + } + + 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; + //return 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) + { + 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 _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 + ? _sqlExpressionFactory.MakeBinary( + sqlUnaryExpression.OperatorType == ExpressionType.Equal + ? ExpressionType.AndAlso + : ExpressionType.OrElse, + left, + right, + sqlUnaryExpression.TypeMapping) + : _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 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 index 1e555aa198d..6ea67f70d02 100644 --- a/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/SqlExpressionOptimizingExpressionVisitor.cs @@ -2,6 +2,7 @@ // 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; using System.Linq.Expressions; using JetBrains.Annotations; @@ -14,31 +15,16 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal public class SqlExpressionOptimizingExpressionVisitor : ExpressionVisitor { private readonly bool _useRelationalNulls; + private readonly IReadOnlyDictionary _parametersValues; - 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) + public SqlExpressionOptimizingExpressionVisitor( + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + bool useRelationalNulls, + [NotNull] IReadOnlyDictionary parametersValues) { SqlExpressionFactory = sqlExpressionFactory; _useRelationalNulls = useRelationalNulls; + _parametersValues = parametersValues; } protected virtual ISqlExpressionFactory SqlExpressionFactory { get; } @@ -56,8 +42,10 @@ protected override Expression VisitExtension(Expression extensionExpression) }; } - private Expression VisitSelectExpression(SelectExpression selectExpression) + protected virtual Expression VisitSelectExpression([NotNull] SelectExpression selectExpression) { + Check.NotNull(selectExpression, nameof(selectExpression)); + var newExpression = base.VisitExtension(selectExpression); // if predicate is optimized to true, we can simply remove it @@ -154,7 +142,11 @@ private SqlExpression SimplifyUnaryExpression( break; - case SqlBinaryExpression binaryOperand: + // these optimizations are only valid in 2-value logic + // NullSemantics removes all nulls from expressions wrapped around Not + // so the optimizations are safe to do as long as UseRelationalNulls = false + case SqlBinaryExpression binaryOperand + when !_useRelationalNulls: { // De Morgan's if (binaryOperand.OperatorType == ExpressionType.AndAlso @@ -172,14 +164,15 @@ private SqlExpression SimplifyUnaryExpression( 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)) + // !(a == b) -> (a != b) + // !(a != b) -> (a == b) + if (binaryOperand.OperatorType == ExpressionType.Equal + || binaryOperand.OperatorType == ExpressionType.NotEqual) { return SimplifyBinaryExpression( - negated, + binaryOperand.OperatorType == ExpressionType.Equal + ? ExpressionType.NotEqual + : ExpressionType.Equal, binaryOperand.Left, binaryOperand.Right, binaryOperand.TypeMapping); @@ -189,98 +182,6 @@ private SqlExpression SimplifyUnaryExpression( } 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); @@ -288,6 +189,8 @@ private SqlExpression SimplifyNullNotNullExpression( protected virtual Expression VisitSqlBinaryExpression([NotNull] SqlBinaryExpression sqlBinaryExpression) { + Check.NotNull(sqlBinaryExpression, nameof(sqlBinaryExpression)); + var newLeft = (SqlExpression)Visit(sqlBinaryExpression.Left); var newRight = (SqlExpression)Visit(sqlBinaryExpression.Right); @@ -332,143 +235,11 @@ private SqlExpression SimplifyBinaryExpression( 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, diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs index 5610cf29666..0fa838d1568 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs @@ -2,7 +2,6 @@ // 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; @@ -46,17 +45,19 @@ public virtual (SelectExpression selectExpression, bool canCache) Optimize( Check.NotNull(parametersValues, nameof(parametersValues)); var canCache = true; + var nullabilityHandlingExpressionVisitor = new NullabilityHandlingExpressionVisitor( + UseRelationalNulls, + Dependencies.SqlExpressionFactory, + parametersValues); - var inExpressionOptimized = new InExpressionValuesExpandingExpressionVisitor( - Dependencies.SqlExpressionFactory, parametersValues).Visit(selectExpression); - - if (!ReferenceEquals(selectExpression, inExpressionOptimized)) + var nullabilityHandled = nullabilityHandlingExpressionVisitor.Visit(selectExpression); + if (!nullabilityHandlingExpressionVisitor.CanCache) { canCache = false; } - var nullParametersOptimized = new ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( - Dependencies.SqlExpressionFactory, UseRelationalNulls, parametersValues).Visit(inExpressionOptimized); + var nullParametersOptimized = new SqlExpressionOptimizingExpressionVisitor( + Dependencies.SqlExpressionFactory, UseRelationalNulls, parametersValues).Visit(nullabilityHandled); var fromSqlParameterOptimized = new FromSqlParameterApplyingExpressionVisitor( Dependencies.SqlExpressionFactory, @@ -71,163 +72,6 @@ public virtual (SelectExpression selectExpression, bool canCache) Optimize( 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 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/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/SqlServerParameterBasedQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs index b7cf63d1973..bdc65f27ed2 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Utilities; @@ -30,7 +31,10 @@ public override (SelectExpression selectExpression, bool canCache) Optimize( var searchConditionOptimized = (SelectExpression)new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory) .Visit(optimizedSelectExpression); - return (searchConditionOptimized, canCache); + var optimized = (SelectExpression)new SqlExpressionOptimizingExpressionVisitor( + Dependencies.SqlExpressionFactory, UseRelationalNulls, parametersValues).Visit(searchConditionOptimized); + + return (optimized, 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.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs index 830dd8f0f51..a7096d34951 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,60 @@ 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 +1116,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 6a35bf8024b..7eb05a94f6c 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.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 68b6ddcf29f..13a67e0a095 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 e470d926334..608efa660b0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs @@ -42,7 +42,7 @@ 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] @@ -50,7 +50,7 @@ WHERE CAST(0 AS bit) = CAST(1 AS bit)", // @"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) @@ -70,11 +70,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) @@ -92,19 +90,17 @@ FROM [FunkyCustomers] AS [f] SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE (@__prm6_0 <> N'') AND (CHARINDEX(@__prm6_0, [f].[FirstName]) <= 0)", +WHERE (@__prm6_0 <> N'') AND NOT (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 (@__prm7_0 <> N'') AND NOT (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) @@ -126,7 +122,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 (([f0].[LastName] <> N'') OR [f0].[LastName] IS NULL) AND NOT (CHARINDEX([f0].[LastName], [f].[FirstName]) > 0)"); } public override async Task String_starts_with_on_argument_with_wildcard_constant(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index b94f6bb2579..0d709de93ef 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -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] @@ -6240,7 +6228,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) @@ -6652,7 +6639,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]"); } @@ -7063,10 +7050,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')"); } @@ -7298,10 +7282,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))"); } @@ -7326,10 +7307,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/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index 71fdcd5636e..ae94618bf01 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -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' @@ -1320,7 +1318,7 @@ public override async Task DefaultIfEmpty_selects_only_required_columns(bool asy FROM ( SELECT NULL AS [empty] ) AS [empty] -LEFT JOIN [Products] AS [p] ON 1 = 1"); +LEFT JOIN [Products] AS [p] ON CAST(1 AS bit) = CAST(1 AS bit)"); } public override async Task Collection_Last_member_access_in_projection_translated(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 46c50276930..61d6ba11d02 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -36,7 +36,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) @@ -46,7 +46,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) @@ -76,7 +76,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) @@ -86,7 +86,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) @@ -1179,7 +1179,7 @@ public override async Task Indexof_with_emptystring(bool async) AssertSql( @"SELECT CASE - WHEN N'' = N'' THEN 0 + WHEN CAST(1 AS bit) = CAST(1 AS bit) THEN 0 ELSE CHARINDEX(N'', [c].[ContactName]) - 1 END FROM [Customers] AS [c] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 7114bb61aa8..77338f23324 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -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/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index a626e3b424d..a112a9104a0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -343,7 +343,7 @@ LEFT JOIN ( SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] WHERE [e].[EmployeeID] = -1 -) AS [t] ON 1 = 1"); +) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit)"); } public override async Task Join_with_default_if_empty_on_both_sources(bool async) @@ -359,7 +359,7 @@ LEFT JOIN ( SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] WHERE [e].[EmployeeID] = -1 -) AS [t] ON 1 = 1 +) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit) INNER JOIN ( SELECT [t0].[EmployeeID], [t0].[City], [t0].[Country], [t0].[FirstName], [t0].[ReportsTo], [t0].[Title] FROM ( @@ -369,7 +369,7 @@ LEFT JOIN ( SELECT [e0].[EmployeeID], [e0].[City], [e0].[Country], [e0].[FirstName], [e0].[ReportsTo], [e0].[Title] FROM [Employees] AS [e0] WHERE [e0].[EmployeeID] = -1 - ) AS [t0] ON 1 = 1 + ) AS [t0] ON CAST(1 AS bit) = CAST(1 AS bit) ) AS [t1] ON [t].[EmployeeID] = [t1].[EmployeeID]"); } @@ -386,7 +386,7 @@ LEFT JOIN ( SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] WHERE [e].[EmployeeID] = -1 -) AS [t] ON 1 = 1"); +) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit)"); } public override async Task Default_if_empty_top_level_positive(bool async) @@ -402,7 +402,7 @@ LEFT JOIN ( SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] WHERE [e].[EmployeeID] > 0 -) AS [t] ON 1 = 1"); +) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit)"); } public override async Task Default_if_empty_top_level_projection(bool async) @@ -418,7 +418,7 @@ LEFT JOIN ( SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] WHERE [e].[EmployeeID] = -1 -) AS [t] ON 1 = 1"); +) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit)"); } public override async Task Where_query_composition(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"); } @@ -3242,7 +3242,7 @@ LEFT JOIN ( 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].[City] = N'London' -) AS [t] ON 1 = 1 +) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit) WHERE [t].[CustomerID] IS NOT NULL"); } @@ -3273,7 +3273,7 @@ LEFT JOIN ( SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] WHERE [o].[OrderID] > 15000 - ) AS [t] ON 1 = 1 + ) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit) ) AS [t0]"); } @@ -3293,7 +3293,7 @@ LEFT JOIN ( SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] WHERE [o].[OrderID] > 15000 - ) AS [t] ON 1 = 1 + ) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit) ) AS [t0] LEFT JOIN [Orders] AS [o0] ON [c].[CustomerID] = [o0].[CustomerID] WHERE ([c].[City] = N'Seattle') AND ([t0].[OrderID] IS NOT NULL AND [o0].[OrderID] IS NOT NULL) @@ -4706,7 +4706,7 @@ LEFT JOIN ( SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] WHERE [e].[EmployeeID] = -1 - ) AS [t] ON 1 = 1) THEN CAST(1 AS bit) + ) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit)) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index 4e46d6c03c2..388fc383950 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 CAST(1 AS bit) = CAST(1 AS bit) THEN 0 ELSE CHARINDEX(N'', [c].[ContactName]) - 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 11da2e0a041..bc1eda003f6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -1111,10 +1111,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"); } @@ -1333,7 +1333,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) @@ -1345,7 +1345,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) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs index 4180492bea1..299b4ac1920 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() @@ -1447,7 +1449,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 CAST(0 AS bit) = CAST(1 AS bit)", + // + @"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 CAST(0 AS bit) = CAST(1 AS bit)", + // + @"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 CAST(0 AS bit) = CAST(1 AS bit)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE CAST(0 AS bit) = CAST(1 AS bit)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]"); } public override void Null_semantics_with_null_check_simple() @@ -1494,6 +1598,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(); @@ -1526,6 +1644,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/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 2947b06cf33..ccefd053802 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -6108,7 +6108,7 @@ WHERE EXISTS ( SELECT 1 FROM [OrganisationUser7973] AS [o0] WHERE [o].[Id] = [o0].[OrganisationId]) - ) AS [t] ON 1 = 1 + ) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit) ) AS [t0]"); } } 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)