From faa7f92424cec082da8bda35517bd92e3a4b797b Mon Sep 17 00:00:00 2001 From: Maurycy Markowski Date: Tue, 7 Jan 2020 18:13:06 -0800 Subject: [PATCH] Additional refactoring of Null Semantics: - moving NullSemantics visitor after 2nd level cache - we need to know the parameter values to properly handle IN expressions wrt null semantics, - NullSemantics visitor needs to go before SqlExpressionOptimizer and SearchCondition, so those two are also moved after 2nd level cache, - combining NullSemantics with SqlExpressionOptimizer (kept SqlExpressionOptimizer name) - SearchCondition now applies some relevant optimization itself, so that we don't need to run full optimizer afterwards, - merging InExpressionValuesExpandingExpressionVisitor into SqlExpressionOptimizer as well, so that we don't apply the rewrite for UseRelationalNulls, - preventing NulSemantics from performing double visitation when computing non-nullable columns. Resolves #11464 Resolves #15722 Resolves #18338 Resolves #18597 Resolves #18689 Resolves #19019 --- ...omSqlParameterApplyingExpressionVisitor.cs | 134 ++ ...lityBasedSqlProcessingExpressionVisitor.cs | 1495 +++++++++++++++++ ...meterBasedQueryTranslationPostprocessor.cs | 308 +--- ...RelationalQueryTranslationPostprocessor.cs | 20 +- .../Query/SqlExpressions/InExpression.cs | 4 +- .../SqlServerServiceCollectionExtensions.cs | 1 - ...rchConditionConvertingExpressionVisitor.cs | 43 +- ...meterBasedQueryTranslationPostprocessor.cs | 6 +- .../Query/NorthwindJoinQueryCosmosTest.cs | 12 + .../Query/NullSemanticsQueryTestBase.cs | 193 ++- .../Query/ComplexNavigationsQueryTestBase.cs | 2 +- .../Query/GearsOfWarQueryTestBase.cs | 14 +- .../Query/NorthwindJoinQueryTestBase.cs | 23 + .../LoadSqlServerTest.cs | 16 +- .../ComplexNavigationsQuerySqlServerTest.cs | 13 +- .../Query/FunkyDataQuerySqlServerTest.cs | 44 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 65 +- .../Query/InheritanceSqlServerTest.cs | 2 +- ...indAggregateOperatorsQuerySqlServerTest.cs | 10 +- .../NorthwindFunctionsQuerySqlServerTest.cs | 12 +- .../NorthwindGroupByQuerySqlServerTest.cs | 4 +- .../Query/NorthwindJoinQuerySqlServerTest.cs | 46 +- ...orthwindMiscellaneousQuerySqlServerTest.cs | 18 +- .../NorthwindSelectQuerySqlServerTest.cs | 2 +- .../Query/NorthwindWhereQuerySqlServerTest.cs | 28 +- .../Query/NullSemanticsQuerySqlServerTest.cs | 249 ++- .../Query/OwnedQuerySqlServerTest.cs | 2 +- .../QueryFilterFuncletizationSqlServerTest.cs | 2 +- .../NorthwindFunctionsQuerySqliteTest.cs | 8 +- .../NorthwindMiscellaneousQuerySqliteTest.cs | 2 +- 30 files changed, 2294 insertions(+), 484 deletions(-) create mode 100644 src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs create mode 100644 src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs diff --git a/src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs new file mode 100644 index 00000000000..17b67401cdd --- /dev/null +++ b/src/EFCore.Relational/Query/FromSqlParameterApplyingExpressionVisitor.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class FromSqlParameterApplyingExpressionVisitor : ExpressionVisitor + { + private readonly IDictionary _visitedFromSqlExpressions + = new Dictionary(ReferenceEqualityComparer.Instance); + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly ParameterNameGenerator _parameterNameGenerator; + private readonly IReadOnlyDictionary _parametersValues; + + public FromSqlParameterApplyingExpressionVisitor( + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + [NotNull] ParameterNameGenerator parameterNameGenerator, + [NotNull] IReadOnlyDictionary parametersValues) + { + Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); + Check.NotNull(parameterNameGenerator, nameof(parameterNameGenerator)); + Check.NotNull(parametersValues, nameof(parametersValues)); + + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGenerator = parameterNameGenerator; + _parametersValues = parametersValues; + } + + public override Expression Visit(Expression expression) + { + if (expression is FromSqlExpression fromSql) + { + if (!_visitedFromSqlExpressions.TryGetValue(fromSql, out var updatedFromSql)) + { + switch (fromSql.Arguments) + { + case ParameterExpression parameterExpression: + var parameterValues = (object[])_parametersValues[parameterExpression.Name]; + + var subParameters = new List(parameterValues.Length); + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < parameterValues.Length; i++) + { + var parameterName = _parameterNameGenerator.GenerateNext(); + if (parameterValues[i] is DbParameter dbParameter) + { + if (string.IsNullOrEmpty(dbParameter.ParameterName)) + { + dbParameter.ParameterName = parameterName; + } + else + { + parameterName = dbParameter.ParameterName; + } + + subParameters.Add(new RawRelationalParameter(parameterName, dbParameter)); + } + else + { + subParameters.Add( + new TypeMappedRelationalParameter( + parameterName, + parameterName, + _sqlExpressionFactory.GetTypeMappingForValue(parameterValues[i]), + parameterValues[i]?.GetType().IsNullableType())); + } + } + + updatedFromSql = new FromSqlExpression( + fromSql.Sql, + Expression.Constant( + new CompositeRelationalParameter( + parameterExpression.Name, + subParameters)), + fromSql.Alias); + + _visitedFromSqlExpressions[fromSql] = updatedFromSql; + break; + + case ConstantExpression constantExpression: + var existingValues = (object[])constantExpression.Value; + var constantValues = new object[existingValues.Length]; + for (var i = 0; i < existingValues.Length; i++) + { + var value = existingValues[i]; + if (value is DbParameter dbParameter) + { + var parameterName = _parameterNameGenerator.GenerateNext(); + if (string.IsNullOrEmpty(dbParameter.ParameterName)) + { + dbParameter.ParameterName = parameterName; + } + else + { + parameterName = dbParameter.ParameterName; + } + + constantValues[i] = new RawRelationalParameter(parameterName, dbParameter); + } + else + { + constantValues[i] = _sqlExpressionFactory.Constant( + value, _sqlExpressionFactory.GetTypeMappingForValue(value)); + } + } + + updatedFromSql = new FromSqlExpression( + fromSql.Sql, + Expression.Constant(constantValues, typeof(object[])), + fromSql.Alias); + + _visitedFromSqlExpressions[fromSql] = updatedFromSql; + break; + } + } + + return updatedFromSql; + } + + return base.Visit(expression); + } + } +} diff --git a/src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs new file mode 100644 index 00000000000..137fddd0383 --- /dev/null +++ b/src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs @@ -0,0 +1,1495 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class NullabilityBasedSqlProcessingExpressionVisitor : SqlExpressionVisitor + { + protected virtual bool UseRelationalNulls { get; } + protected virtual ISqlExpressionFactory SqlExpressionFactory { get; } + protected virtual IReadOnlyDictionary ParameterValues { get; } + protected virtual List NonNullableColumns { get; } = new List(); + + protected virtual bool CanCache { get; set; } + + private bool _nullable; + private bool _allowOptimizedExpansion; + + public NullabilityBasedSqlProcessingExpressionVisitor( + [NotNull] ISqlExpressionFactory sqlExpressionFactory, + [NotNull] IReadOnlyDictionary parameterValues, + bool useRelationalNulls) + { + SqlExpressionFactory = sqlExpressionFactory; + ParameterValues = parameterValues; + UseRelationalNulls = useRelationalNulls; + CanCache = true; + + _allowOptimizedExpansion = false; + } + + private void RestoreNonNullableColumnsList(int counter) + { + if (counter < NonNullableColumns.Count) + { + NonNullableColumns.RemoveRange(counter, NonNullableColumns.Count - counter); + } + } + + public virtual (SelectExpression selectExpression, bool canCache) Process([NotNull] SelectExpression selectExpression) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + + return (selectExpression: VisitInternal(selectExpression).ResultExpression, canCache: CanCache); + } + + /// + /// Method that handles visitation of SqlExpression nodes. All provider specific nodes should be handled by this method in the provider specific implementation of . + /// Depending on the settings, the method sets up state for the actual visitation, cleans up after the visitation is complete + /// and returns resulting expression along with it's nullability. + /// + /// Type of the resulting expression. + /// Expression that is to be visited. + /// True if null semantics inside the expression can be expanded in the optimized way (i.e. 'null' and 'false' are interchangable), false otherwise. + /// True if method should reset the number of non-nullable columns after the visitation is complete, false otherwise. + /// Tuple representing visited expression and it's nullability. + protected virtual (TResult ResultExpression, bool Nullable) VisitInternal( + [CanBeNull] Expression expression, + bool allowOptimizedExpansion = false, + bool restoreNonNullableColumnInformation = true) + where TResult : Expression + { + if (expression != null) + { + return (null, false); + } + + _nullable = false; + var currentNonNullableColumnsCount = NonNullableColumns.Count; + var previousAllowOptimizedExpansion = _allowOptimizedExpansion; + _allowOptimizedExpansion = allowOptimizedExpansion; + var resultExpression = (TResult)Visit(expression); + _allowOptimizedExpansion = previousAllowOptimizedExpansion; + if (restoreNonNullableColumnInformation) + { + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + return (resultExpression, _nullable); + } + + protected override Expression VisitCase(CaseExpression caseExpression) + { + Check.NotNull(caseExpression, nameof(caseExpression)); + + // 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 nullable = caseExpression.ElseResult == null; + var currentNonNullableColumnsCount = NonNullableColumns.Count; + + var operand = VisitInternal(caseExpression.Operand).ResultExpression; + var whenClauses = new List(); + var testIsCondition = caseExpression.Operand == null; + foreach (var whenClause in caseExpression.WhenClauses) + { + // we can use non-nullable column information we got from visiting Test, in the Result + var newTest = VisitInternal(whenClause.Test, allowOptimizedExpansion: testIsCondition, restoreNonNullableColumnInformation: false).ResultExpression; + var (newResult, resultNullable) = VisitInternal(whenClause.Result); + + nullable |= resultNullable; + whenClauses.Add(new CaseWhenClause(newTest, newResult)); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + var (elseResult, elseResultNullable) = VisitInternal(caseExpression.ElseResult); + _nullable = nullable || elseResultNullable; + + return caseExpression.Update(operand, whenClauses, elseResult); + } + + protected override Expression VisitColumn(ColumnExpression columnExpression) + { + Check.NotNull(columnExpression, nameof(columnExpression)); + + _nullable = columnExpression.IsNullable && !NonNullableColumns.Contains(columnExpression); + + return columnExpression; + } + + protected override Expression VisitCrossApply(CrossApplyExpression crossApplyExpression) + { + Check.NotNull(crossApplyExpression, nameof(crossApplyExpression)); + + return crossApplyExpression.Update( + VisitInternal(crossApplyExpression.Table).ResultExpression); + } + + protected override Expression VisitCrossJoin(CrossJoinExpression crossJoinExpression) + { + Check.NotNull(crossJoinExpression, nameof(crossJoinExpression)); + + return crossJoinExpression.Update( + VisitInternal(crossJoinExpression.Table).ResultExpression); + } + + protected override Expression VisitExcept(ExceptExpression exceptExpression) + { + Check.NotNull(exceptExpression, nameof(exceptExpression)); + + var source1 = VisitInternal(exceptExpression.Source1).ResultExpression; + var source2 = VisitInternal(exceptExpression.Source2).ResultExpression; + + return exceptExpression.Update(source1, source2); + } + + protected override Expression VisitExists(ExistsExpression existsExpression) + { + Check.NotNull(existsExpression, nameof(existsExpression)); + + return existsExpression.Update( + VisitInternal(existsExpression.Subquery).ResultExpression); + } + + protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) + => Check.NotNull(fromSqlExpression, nameof(fromSqlExpression)); + + protected override Expression VisitIn(InExpression inExpression) + { + Check.NotNull(inExpression, nameof(inExpression)); + + var (item, itemNullable) = VisitInternal(inExpression.Item); + + if (inExpression.Subquery != null) + { + var (subquery, subqueryNullable) = VisitInternal(inExpression.Subquery); + _nullable = itemNullable || subqueryNullable; + + return inExpression.Update(item, values: null, subquery); + } + + // for relational null semantics just leave as is + // same for values we don't know how to properly handle (i.e. other than constant or parameter) + if (UseRelationalNulls + || !(inExpression.Values is SqlConstantExpression || inExpression.Values is SqlParameterExpression)) + { + var (values, valuesNullable) = VisitInternal(inExpression.Values); + _nullable = itemNullable || valuesNullable; + + 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 (inValuesExpression, inValuesList, hasNullValue) = ProcessInExpressionValues(inExpression.Values); + + // either values array is empty or only contains null + if (inValuesList.Count == 0) + { + _nullable = 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 + || (_allowOptimizedExpansion && !inExpression.IsNegated && !hasNullValue)) + { + _nullable = 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, inValuesExpression, subquery: null); + } + + // adding null comparison term to remove nulls completely from the resulting expression + _nullable = 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, inValuesExpression, subquery: null), + SqlExpressionFactory.IsNotNull(item)) + : SqlExpressionFactory.OrElse( + inExpression.Update(item, inValuesExpression, subquery: null), + SqlExpressionFactory.IsNull(item)); + + (SqlConstantExpression ProcessedValuesExpression, List ProcessedValuesList, bool HasNullValue) ProcessInExpressionValues(SqlExpression valuesExpression) + { + var inValues = new List(); + var hasNullValue = false; + RelationalTypeMapping typeMapping = null; + + IEnumerable values = null; + if (valuesExpression is SqlConstantExpression sqlConstant) + { + typeMapping = sqlConstant.TypeMapping; + values = (IEnumerable)sqlConstant.Value; + } + else 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); + } + + var processedValuesExpression = SqlExpressionFactory.Constant(inValues, typeMapping); + + return (processedValuesExpression, inValues, hasNullValue); + } + } + + protected override Expression VisitInnerJoin(InnerJoinExpression innerJoinExpression) + { + Check.NotNull(innerJoinExpression, nameof(innerJoinExpression)); + + var newTable = VisitInternal(innerJoinExpression.Table).ResultExpression; + var newJoinPredicate = VisitJoinPredicate((SqlBinaryExpression)innerJoinExpression.JoinPredicate); + + return newJoinPredicate is SqlConstantExpression constantJoinPredicate + && constantJoinPredicate.Value is bool boolPredicate + && boolPredicate + ? (Expression)new CrossJoinExpression(newTable) + : innerJoinExpression.Update(newTable, newJoinPredicate); + } + + protected override Expression VisitIntersect(IntersectExpression intersectExpression) + { + Check.NotNull(intersectExpression, nameof(intersectExpression)); + + var source1 = VisitInternal(intersectExpression.Source1).ResultExpression; + var source2 = VisitInternal(intersectExpression.Source2).ResultExpression; + + return intersectExpression.Update(source1, source2); + } + + protected override Expression VisitLeftJoin(LeftJoinExpression leftJoinExpression) + { + Check.NotNull(leftJoinExpression, nameof(leftJoinExpression)); + + var newTable = VisitInternal(leftJoinExpression.Table).ResultExpression; + var newJoinPredicate = VisitJoinPredicate((SqlBinaryExpression)leftJoinExpression.JoinPredicate); + + return leftJoinExpression.Update(newTable, newJoinPredicate); + } + + private SqlExpression VisitJoinPredicate(SqlBinaryExpression predicate) + { + switch (predicate.OperatorType) + { + case ExpressionType.Equal: + { + var (left, leftNullable) = VisitInternal(predicate.Left, allowOptimizedExpansion: true); + var (right, rightNullable) = VisitInternal(predicate.Right, allowOptimizedExpansion: true); + + var result = OptimizeComparison( + predicate.Update(left, right), + left, + right, + leftNullable, + rightNullable); + + return result; + } + + case ExpressionType.AndAlso: + return VisitInternal(predicate, allowOptimizedExpansion: true).ResultExpression; + + default: + throw new InvalidOperationException("Unexpected join predicate shape: " + predicate); + } + } + + protected override Expression VisitLike(LikeExpression likeExpression) + { + var (match, matchNullable) = VisitInternal(likeExpression.Match); + var (pattern, patternNullable) = VisitInternal(likeExpression.Pattern); + var (escapeChar, escapeCharNullable) = VisitInternal(likeExpression.EscapeChar); + _nullable = matchNullable || patternNullable || escapeCharNullable; + + return likeExpression.Update(match, pattern, escapeChar); + } + + protected override Expression VisitOrdering(OrderingExpression orderingExpression) + { + Check.NotNull(orderingExpression, nameof(orderingExpression)); + + return orderingExpression.Update( + VisitInternal(orderingExpression.Expression).ResultExpression); + } + + protected override Expression VisitOuterApply(OuterApplyExpression outerApplyExpression) + { + Check.NotNull(outerApplyExpression, nameof(outerApplyExpression)); + + return outerApplyExpression.Update( + VisitInternal(outerApplyExpression.Table).ResultExpression); + } + + protected override Expression VisitProjection(ProjectionExpression projectionExpression) + { + Check.NotNull(projectionExpression, nameof(projectionExpression)); + + return projectionExpression.Update( + VisitInternal(projectionExpression.Expression).ResultExpression); + } + + protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression) + { + Check.NotNull(rowNumberExpression, nameof(rowNumberExpression)); + + var changed = false; + var partitions = new List(); + foreach (var partition in rowNumberExpression.Partitions) + { + var newPartition = VisitInternal(partition).ResultExpression; + changed |= newPartition != partition; + partitions.Add(newPartition); + } + + var orderings = new List(); + foreach (var ordering in rowNumberExpression.Orderings) + { + var newOrdering = VisitInternal(ordering).ResultExpression; + changed |= newOrdering != ordering; + orderings.Add(newOrdering); + } + + return rowNumberExpression.Update(partitions, orderings); + } + + protected override Expression VisitScalarSubquery(ScalarSubqueryExpression scalarSubqueryExpression) + { + Check.NotNull(scalarSubqueryExpression, nameof(scalarSubqueryExpression)); + + return scalarSubqueryExpression.Update( + VisitInternal(scalarSubqueryExpression.Subquery).ResultExpression); + } + + protected override Expression VisitSelect(SelectExpression selectExpression) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + + var changed = false; + var projections = new List(); + foreach (var item in selectExpression.Projection) + { + var updatedProjection = VisitInternal(item).ResultExpression; + projections.Add(updatedProjection); + changed |= updatedProjection != item; + } + + var tables = new List(); + foreach (var table in selectExpression.Tables) + { + var newTable = VisitInternal(table).ResultExpression; + changed |= newTable != table; + tables.Add(newTable); + } + + var predicate = VisitInternal(selectExpression.Predicate, allowOptimizedExpansion: true).ResultExpression; + changed |= predicate != selectExpression.Predicate; + + if (predicate is SqlConstantExpression predicateConstantExpression + && predicateConstantExpression.Value is bool predicateBoolValue + && predicateBoolValue) + { + predicate = null; + changed = true; + } + + var groupBy = new List(); + foreach (var groupingKey in selectExpression.GroupBy) + { + var newGroupingKey = VisitInternal(groupingKey).ResultExpression; + changed |= newGroupingKey != groupingKey; + groupBy.Add(newGroupingKey); + } + + var having = VisitInternal(selectExpression.Having, allowOptimizedExpansion: true).ResultExpression; + changed |= having != selectExpression.Having; + + if (having is SqlConstantExpression havingConstantExpression + && havingConstantExpression.Value is bool havingBoolValue + && havingBoolValue) + { + having = null; + changed = true; + } + + var orderings = new List(); + foreach (var ordering in selectExpression.Orderings) + { + var orderingExpression = VisitInternal(ordering.Expression).ResultExpression; + changed |= orderingExpression != ordering.Expression; + orderings.Add(ordering.Update(orderingExpression)); + } + + var offset = VisitInternal(selectExpression.Offset).ResultExpression; + changed |= offset != selectExpression.Offset; + + var limit = VisitInternal(selectExpression.Limit).ResultExpression; + changed |= limit != selectExpression.Limit; + + // SelectExpression can always yield null + // (e.g. projecting non-nullable column but with predicate that filters out all rows) + _nullable = true; + + return changed + ? selectExpression.Update( + projections, tables, predicate, groupBy, having, orderings, limit, offset, selectExpression.IsDistinct, + selectExpression.Alias) + : selectExpression; + } + + protected override Expression VisitSqlBinary(SqlBinaryExpression sqlBinaryExpression) + { + Check.NotNull(sqlBinaryExpression, nameof(sqlBinaryExpression)); + + _nullable = false; + var optimize = _allowOptimizedExpansion; + + _allowOptimizedExpansion = _allowOptimizedExpansion + && (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso + || sqlBinaryExpression.OperatorType == ExpressionType.OrElse); + + var currentNonNullableColumnsCount = NonNullableColumns.Count; + + var (left, leftNullable) = VisitInternal( + sqlBinaryExpression.Left, + allowOptimizedExpansion: _allowOptimizedExpansion, + restoreNonNullableColumnInformation: false); + + var leftNonNullableColumns = NonNullableColumns.Skip(currentNonNullableColumnsCount).ToList(); + if (sqlBinaryExpression.OperatorType != ExpressionType.AndAlso) + { + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + } + + var (right, rightNullable) = VisitInternal( + sqlBinaryExpression.Right, + allowOptimizedExpansion: _allowOptimizedExpansion, + restoreNonNullableColumnInformation: false); + + if (sqlBinaryExpression.OperatorType == ExpressionType.OrElse) + { + var intersect = leftNonNullableColumns.Intersect(NonNullableColumns.Skip(currentNonNullableColumnsCount)).ToList(); + RestoreNonNullableColumnsList(currentNonNullableColumnsCount); + 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); + } + + // nullableStringColumn + NULL -> COALESCE(nullableStringColumn, "") + "" + if (sqlBinaryExpression.OperatorType == ExpressionType.Add + && sqlBinaryExpression.Type == typeof(string)) + { + if (leftNullable) + { + left = AddNullConcatenationProtection(left, sqlBinaryExpression.TypeMapping); + } + + if (rightNullable) + { + right = AddNullConcatenationProtection(right, sqlBinaryExpression.TypeMapping); + } + + return sqlBinaryExpression.Update(left, right); + } + + if (sqlBinaryExpression.OperatorType == ExpressionType.Equal + || sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) + { + var updated = sqlBinaryExpression.Update(left, right); + + var optimized = OptimizeComparison( + updated, + left, + right, + leftNullable, + rightNullable); + + 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.Equals(updated) + && (leftNullable || rightNullable) + && !UseRelationalNulls) + { + var rewriteNullSemanticsResult = RewriteNullSemantics( + updated, + updated.Left, + updated.Right, + leftNullable, + rightNullable, + optimize); + + _allowOptimizedExpansion = optimize; + + return rewriteNullSemanticsResult; + } + + _allowOptimizedExpansion = optimize; + + return optimized; + } + + _nullable = leftNullable || rightNullable; + _allowOptimizedExpansion = optimize; + + var result = sqlBinaryExpression.Update(left, right); + + return result is SqlBinaryExpression sqlBinaryResult + && (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso + || sqlBinaryExpression.OperatorType == ExpressionType.OrElse) + ? SimplifyLogicalSqlBinaryExpression(sqlBinaryResult) + : result; + + SqlExpression AddNullConcatenationProtection(SqlExpression argument, RelationalTypeMapping typeMapping) + => argument is SqlConstantExpression || argument is SqlParameterExpression + ? (SqlExpression)SqlExpressionFactory.Constant(string.Empty, typeMapping) + : SqlExpressionFactory.Coalesce(argument, SqlExpressionFactory.Constant(string.Empty, typeMapping)); + } + + private SqlExpression OptimizeComparison( + SqlBinaryExpression sqlBinaryExpression, + SqlExpression left, + SqlExpression right, + bool leftNullable, + bool rightNullable) + { + 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); + + _nullable = false; + + 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); + + _nullable = false; + + return result; + } + + if (IsTrueOrFalse(right) is bool rightTrueFalseValue + && !leftNullable) + { + _nullable = leftNullable; + + // only correct in 2-value logic + // a == true -> a + // a == false -> !a + // a != true -> !a + // a != false -> a + return sqlBinaryExpression.OperatorType == ExpressionType.Equal ^ rightTrueFalseValue + ? OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(left)) + : left; + } + + if (IsTrueOrFalse(left) is bool leftTrueFalseValue + && !rightNullable) + { + _nullable = rightNullable; + + // 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)) + { + _nullable = false; + + 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 optimize) + { + var leftUnary = left as SqlUnaryExpression; + var rightUnary = right as SqlUnaryExpression; + + var leftNegated = leftUnary?.IsLogicalNot() == true; + var rightNegated = rightUnary?.IsLogicalNot() == true; + + if (leftNegated) + { + left = leftUnary.Operand; + } + + if (rightNegated) + { + right = rightUnary.Operand; + } + + var leftIsNull = ProcessNullNotNull(SqlExpressionFactory.IsNull(left), leftNullable); + var leftIsNotNull = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(leftIsNull)); + + var rightIsNull = ProcessNullNotNull(SqlExpressionFactory.IsNull(right), rightNullable); + var rightIsNotNull = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(rightIsNull)); + + // optimized expansion which doesn't distinguish between null and false + if (optimize + && sqlBinaryExpression.OperatorType == ExpressionType.Equal + && !leftNegated + && !rightNegated) + { + // when we use optimized form, the result can still be nullable + if (leftNullable && rightNullable) + { + _nullable = true; + + return SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.Equal(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)))); + } + + if ((leftNullable && !rightNullable) + || (!leftNullable && rightNullable)) + { + _nullable = true; + + return SqlExpressionFactory.Equal(left, right); + } + } + + // doing a full null semantics rewrite - removing all nulls from truth table + _nullable = false; + + if (sqlBinaryExpression.OperatorType == ExpressionType.Equal) + { + if (leftNullable && rightNullable) + { + // ?a == ?b <=> !(?a) == !(?b) -> [(a == b) && (a != null && b != null)] || (a == null && b == null)) + // !(?a) == ?b <=> ?a == !(?b) -> [(a != b) && (a != null && b != null)] || (a == null && b == null) + return leftNegated == rightNegated + ? ExpandNullableEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull) + : ExpandNegatedNullableEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull); + } + + if (leftNullable && !rightNullable) + { + // ?a == b <=> !(?a) == !b -> (a == b) && (a != null) + // !(?a) == b <=> ?a == !b -> (a != b) && (a != null) + return leftNegated == rightNegated + ? ExpandNullableEqualNonNullable(left, right, leftIsNotNull) + : ExpandNegatedNullableEqualNonNullable(left, right, leftIsNotNull); + } + + if (rightNullable && !leftNullable) + { + // a == ?b <=> !a == !(?b) -> (a == b) && (b != null) + // !a == ?b <=> a == !(?b) -> (a != b) && (b != null) + return leftNegated == rightNegated + ? ExpandNullableEqualNonNullable(left, right, rightIsNotNull) + : ExpandNegatedNullableEqualNonNullable(left, right, rightIsNotNull); + } + } + + if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) + { + if (leftNullable && rightNullable) + { + // ?a != ?b <=> !(?a) != !(?b) -> [(a != b) || (a == null || b == null)] && (a != null || b != null) + // !(?a) != ?b <=> ?a != !(?b) -> [(a == b) || (a == null || b == null)] && (a != null || b != null) + return leftNegated == rightNegated + ? ExpandNullableNotEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull) + : ExpandNegatedNullableNotEqualNullable(left, right, leftIsNull, leftIsNotNull, rightIsNull, rightIsNotNull); + } + + if (leftNullable && !rightNullable) + { + // ?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 && !leftNullable) + { + // a != ?b <=> !a != !(?b) -> (a != b) || (b == null) + // !a != ?b <=> a != !(?b) -> (a == b) || (b == null) + return leftNegated == rightNegated + ? ExpandNullableNotEqualNonNullable(left, right, rightIsNull) + : ExpandNegatedNullableNotEqualNonNullable(left, right, rightIsNull); + } + } + + return sqlBinaryExpression.Update(left, right); + } + + private SqlExpression SimplifyLogicalSqlBinaryExpression( + SqlBinaryExpression sqlBinaryExpression) + { + var leftUnary = sqlBinaryExpression.Left as SqlUnaryExpression; + var rightUnary = sqlBinaryExpression.Right as SqlUnaryExpression; + if (leftUnary != null + && rightUnary != null + && (leftUnary.OperatorType == ExpressionType.Equal || leftUnary.OperatorType == ExpressionType.NotEqual) + && (rightUnary.OperatorType == ExpressionType.Equal || rightUnary.OperatorType == ExpressionType.NotEqual) + && leftUnary.Operand.Equals(rightUnary.Operand)) + { + // a is null || a is null -> a is null + // a is not null || a is not null -> a is not null + // a is null && a is null -> a is null + // a is not null && a is not null -> a is not null + // a is null || a is not null -> true + // a is null && a is not null -> false + return leftUnary.OperatorType == rightUnary.OperatorType + ? (SqlExpression)leftUnary + : SqlExpressionFactory.Constant(sqlBinaryExpression.OperatorType == ExpressionType.OrElse, sqlBinaryExpression.TypeMapping); + } + + // true && a -> a + // true || a -> true + // false && a -> false + // false || a -> a + if (sqlBinaryExpression.Left is SqlConstantExpression newLeftConstant) + { + return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso + ? (bool)newLeftConstant.Value + ? sqlBinaryExpression.Right + : newLeftConstant + : (bool)newLeftConstant.Value + ? newLeftConstant + : sqlBinaryExpression.Right; + } + else if (sqlBinaryExpression.Right is SqlConstantExpression newRightConstant) + { + // a && true -> a + // a || true -> true + // a && false -> false + // a || false -> a + return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso + ? (bool)newRightConstant.Value + ? sqlBinaryExpression.Left + : newRightConstant + : (bool)newRightConstant.Value + ? newRightConstant + : sqlBinaryExpression.Left; + } + + return sqlBinaryExpression; + } + + protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) + { + Check.NotNull(sqlConstantExpression, nameof(sqlConstantExpression)); + + _nullable = 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)); + + if (sqlFunctionExpression.IsBuiltIn + && string.Equals(sqlFunctionExpression.Name, "COALESCE", StringComparison.OrdinalIgnoreCase)) + { + var (left, leftNullable) = VisitInternal(sqlFunctionExpression.Arguments[0]); + var (right, rightNullable) = VisitInternal(sqlFunctionExpression.Arguments[1]); + _nullable = leftNullable && rightNullable; + + return sqlFunctionExpression.Update(sqlFunctionExpression.Instance, new[] { left, right }); + } + + var (instance, _) = VisitInternal(sqlFunctionExpression.Instance); + + if (sqlFunctionExpression.IsNiladic) + { + // TODO: #18555 + _nullable = true; + + return sqlFunctionExpression.Update(instance, sqlFunctionExpression.Arguments); + } + + var arguments = new SqlExpression[sqlFunctionExpression.Arguments.Count]; + for (var i = 0; i < arguments.Length; i++) + { + (arguments[i], _) = VisitInternal(sqlFunctionExpression.Arguments[i]); + } + + // TODO: #18555 + _nullable = true; + + return sqlFunctionExpression.Update(instance, arguments); + } + + protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression) + { + Check.NotNull(sqlParameterExpression, nameof(sqlParameterExpression)); + + _nullable = ParameterValues[sqlParameterExpression.Name] == null; + + return _nullable + ? SqlExpressionFactory.Constant(null, sqlParameterExpression.TypeMapping) + : (SqlExpression)sqlParameterExpression; + } + + protected override Expression VisitSqlUnary(SqlUnaryExpression sqlUnaryExpression) + { + Check.NotNull(sqlUnaryExpression, nameof(sqlUnaryExpression)); + + var (operand, operandNullable) = VisitInternal(sqlUnaryExpression.Operand); + var updated = sqlUnaryExpression.Update(operand); + + if (sqlUnaryExpression.OperatorType == ExpressionType.Equal + || sqlUnaryExpression.OperatorType == ExpressionType.NotEqual) + { + var result = ProcessNullNotNull(updated, operandNullable); + + // result of IsNull/IsNotNull can never be null + _nullable = false; + + if (result is SqlUnaryExpression resultUnary + && resultUnary.OperatorType == ExpressionType.NotEqual + && resultUnary.Operand is ColumnExpression resultColumnOperand) + { + NonNullableColumns.Add(resultColumnOperand); + } + + return result; + } + + return !_nullable && sqlUnaryExpression.OperatorType == ExpressionType.Not + ? OptimizeNonNullableNotExpression(updated) + : updated; + } + + private SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression) + { + if (sqlUnaryExpression.OperatorType != ExpressionType.Not) + { + return sqlUnaryExpression; + } + + switch (sqlUnaryExpression.Operand) + { + // !(true) -> false + // !(false) -> true + case SqlConstantExpression constantOperand + when constantOperand.Value is bool value: + { + return SqlExpressionFactory.Constant(!value, sqlUnaryExpression.TypeMapping); + } + + case InExpression inOperand: + return inOperand.Negate(); + + case SqlUnaryExpression sqlUnaryOperand: + { + switch (sqlUnaryOperand.OperatorType) + { + // !(!a) -> a + case ExpressionType.Not: + return sqlUnaryOperand.Operand; + + //!(a IS NULL) -> a IS NOT NULL + case ExpressionType.Equal: + return SqlExpressionFactory.IsNotNull(sqlUnaryOperand.Operand); + + //!(a IS NOT NULL) -> a IS NULL + case ExpressionType.NotEqual: + return SqlExpressionFactory.IsNull(sqlUnaryOperand.Operand); + } + break; + } + + case SqlBinaryExpression sqlBinaryOperand: + { + // optimizations below are only correct in 2-value logic + // De Morgan's + if (sqlBinaryOperand.OperatorType == ExpressionType.AndAlso + || sqlBinaryOperand.OperatorType == ExpressionType.OrElse) + { + // since entire AndAlso/OrElse expression is non-nullable, both sides of it (left and right) must also be non-nullable + // so it's safe to perform recursive optimization here + var left = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(sqlBinaryOperand.Left)); + var right = OptimizeNonNullableNotExpression(SqlExpressionFactory.Not(sqlBinaryOperand.Right)); + + return SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.MakeBinary( + sqlBinaryOperand.OperatorType == ExpressionType.AndAlso + ? ExpressionType.OrElse + : ExpressionType.AndAlso, + left, + right, + sqlBinaryOperand.TypeMapping)); + } + + // !(a == b) -> a != b + // !(a != b) -> a == b + // !(a > b) -> a <= b + // !(a >= b) -> a < b + // !(a < b) -> a >= b + // !(a <= b) -> a > b + if (TryNegate(sqlBinaryOperand.OperatorType, out var negated)) + { + return SqlExpressionFactory.MakeBinary( + negated, + sqlBinaryOperand.Left, + sqlBinaryOperand.Right, + sqlBinaryOperand.TypeMapping); + } + } + break; + } + + 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( + sqlUnaryExpression.Update(sqlUnaryOperand.Operand), + 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 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 SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.MakeBinary( + sqlUnaryExpression.OperatorType == ExpressionType.Equal + ? ExpressionType.OrElse + : ExpressionType.AndAlso, + left, + right, + sqlUnaryExpression.TypeMapping)); + } + + case SqlFunctionExpression sqlFunctionExpression + when sqlFunctionExpression.IsBuiltIn && string.Equals("COALESCE", sqlFunctionExpression.Name, StringComparison.OrdinalIgnoreCase): + { + // for coalesce: + // (a ?? b) == null -> a == null && b == null + // (a ?? b) != null -> a != null || b != null + var left = ProcessNullNotNull( + SqlExpressionFactory.MakeUnary( + sqlUnaryExpression.OperatorType, + sqlFunctionExpression.Arguments[0], + typeof(bool), + sqlUnaryExpression.TypeMapping), + operandNullable: null); + + var right = ProcessNullNotNull( + SqlExpressionFactory.MakeUnary( + sqlUnaryExpression.OperatorType, + sqlFunctionExpression.Arguments[1], + typeof(bool), + sqlUnaryExpression.TypeMapping), + operandNullable: null); + + return SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.MakeBinary( + sqlUnaryExpression.OperatorType == ExpressionType.Equal + ? ExpressionType.AndAlso + : ExpressionType.OrElse, + 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 source1 = VisitInternal(unionExpression.Source1).ResultExpression; + var source2 = VisitInternal(unionExpression.Source2).ResultExpression; + + return unionExpression.Update(source1, source2); + } + + // ?a == ?b -> [(a == b) && (a != null && b != null)] || (a == null && b == null)) + // + // a | b | F1 = a == b | F2 = (a != null && b != null) | F3 = F1 && F2 | + // | | | | | + // 0 | 0 | 1 | 1 | 1 | + // 0 | 1 | 0 | 1 | 0 | + // 0 | N | N | 0 | 0 | + // 1 | 0 | 0 | 1 | 0 | + // 1 | 1 | 1 | 1 | 1 | + // 1 | N | N | 0 | 0 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + // N | N | N | 0 | 0 | + // + // a | b | F4 = (a == null && b == null) | Final = F3 OR F4 | + // | | | | + // 0 | 0 | 0 | 1 OR 0 = 1 | + // 0 | 1 | 0 | 0 OR 0 = 0 | + // 0 | N | 0 | 0 OR 0 = 0 | + // 1 | 0 | 0 | 0 OR 0 = 0 | + // 1 | 1 | 0 | 1 OR 0 = 1 | + // 1 | N | 0 | 0 OR 0 = 0 | + // N | 0 | 0 | 0 OR 0 = 0 | + // N | 1 | 0 | 0 OR 0 = 0 | + // N | N | 1 | 0 OR 1 = 1 | + private SqlExpression ExpandNullableEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.Equal(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso(leftIsNotNull, rightIsNotNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)))); + + // !(?a) == ?b -> [(a != b) && (a != null && b != null)] || (a == null && b == null) + // + // a | b | F1 = a != b | F2 = (a != null && b != null) | F3 = F1 && F2 | + // | | | | | + // 0 | 0 | 0 | 1 | 0 | + // 0 | 1 | 1 | 1 | 1 | + // 0 | N | N | 0 | 0 | + // 1 | 0 | 1 | 1 | 1 | + // 1 | 1 | 0 | 1 | 0 | + // 1 | N | N | 0 | 0 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + // N | N | N | 0 | 0 | + // + // a | b | F4 = (a == null && b == null) | Final = F3 OR F4 | + // | | | | + // 0 | 0 | 0 | 0 OR 0 = 0 | + // 0 | 1 | 0 | 1 OR 0 = 1 | + // 0 | N | 0 | 0 OR 0 = 0 | + // 1 | 0 | 0 | 1 OR 0 = 1 | + // 1 | 1 | 0 | 0 OR 0 = 0 | + // 1 | N | 0 | 0 OR 0 = 0 | + // N | 0 | 0 | 0 OR 0 = 0 | + // N | 1 | 0 | 0 OR 0 = 0 | + // N | N | 1 | 0 OR 1 = 1 | + private SqlExpression ExpandNegatedNullableEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.NotEqual(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso(leftIsNotNull, rightIsNotNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)))); + + // ?a == b -> (a == b) && (a != null) + // + // a | b | F1 = a == b | F2 = (a != null) | Final = F1 && F2 | + // | | | | | + // 0 | 0 | 1 | 1 | 1 | + // 0 | 1 | 0 | 1 | 0 | + // 1 | 0 | 0 | 1 | 0 | + // 1 | 1 | 1 | 1 | 1 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + private SqlExpression ExpandNullableEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.Equal(left, right), + leftIsNotNull)); + + // !(?a) == b -> (a != b) && (a != null) + // + // a | b | F1 = a != b | F2 = (a != null) | Final = F1 && F2 | + // | | | | | + // 0 | 0 | 0 | 1 | 0 | + // 0 | 1 | 1 | 1 | 1 | + // 1 | 0 | 1 | 1 | 1 | + // 1 | 1 | 0 | 1 | 0 | + // N | 0 | N | 0 | 0 | + // N | 1 | N | 0 | 0 | + private SqlExpression ExpandNegatedNullableEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SqlExpressionFactory.NotEqual(left, right), + leftIsNotNull)); + + // ?a != ?b -> [(a != b) || (a == null || b == null)] && (a != null || b != null) + // + // a | b | F1 = a != b | F2 = (a == null || b == null) | F3 = F1 || F2 | + // | | | | | + // 0 | 0 | 0 | 0 | 0 | + // 0 | 1 | 1 | 0 | 1 | + // 0 | N | N | 1 | 1 | + // 1 | 0 | 1 | 0 | 1 | + // 1 | 1 | 0 | 0 | 0 | + // 1 | N | N | 1 | 1 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + // N | N | N | 1 | 1 | + // + // a | b | F4 = (a != null || b != null) | Final = F3 && F4 | + // | | | | + // 0 | 0 | 1 | 0 && 1 = 0 | + // 0 | 1 | 1 | 1 && 1 = 1 | + // 0 | N | 1 | 1 && 1 = 1 | + // 1 | 0 | 1 | 1 && 1 = 1 | + // 1 | 1 | 1 | 0 && 1 = 0 | + // 1 | N | 1 | 1 && 1 = 1 | + // N | 0 | 1 | 1 && 1 = 1 | + // N | 1 | 1 | 1 && 1 = 1 | + // N | N | 0 | 1 && 0 = 0 | + private SqlExpression ExpandNullableNotEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.NotEqual(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse(leftIsNull, rightIsNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse(leftIsNotNull, rightIsNotNull)))); + + // !(?a) != ?b -> [(a == b) || (a == null || b == null)] && (a != null || b != null) + // + // a | b | F1 = a == b | F2 = (a == null || b == null) | F3 = F1 || F2 | + // | | | | | + // 0 | 0 | 1 | 0 | 1 | + // 0 | 1 | 0 | 0 | 0 | + // 0 | N | N | 1 | 1 | + // 1 | 0 | 0 | 0 | 0 | + // 1 | 1 | 1 | 0 | 1 | + // 1 | N | N | 1 | 1 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + // N | N | N | 1 | 1 | + // + // a | b | F4 = (a != null || b != null) | Final = F3 && F4 | + // | | | | + // 0 | 0 | 1 | 1 && 1 = 1 | + // 0 | 1 | 1 | 0 && 1 = 0 | + // 0 | N | 1 | 1 && 1 = 1 | + // 1 | 0 | 1 | 0 && 1 = 0 | + // 1 | 1 | 1 | 1 && 1 = 1 | + // 1 | N | 1 | 1 && 1 = 1 | + // N | 0 | 1 | 1 && 1 = 1 | + // N | 1 | 1 | 1 && 1 = 1 | + // N | N | 0 | 1 && 0 = 0 | + private SqlExpression ExpandNegatedNullableNotEqualNullable( + SqlExpression left, + SqlExpression right, + SqlExpression leftIsNull, + SqlExpression leftIsNotNull, + SqlExpression rightIsNull, + SqlExpression rightIsNotNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.AndAlso( + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.Equal(left, right), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse(leftIsNull, rightIsNull)))), + SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse(leftIsNotNull, rightIsNotNull)))); + + // ?a != b -> (a != b) || (a == null) + // + // a | b | F1 = a != b | F2 = (a == null) | Final = F1 OR F2 | + // | | | | | + // 0 | 0 | 0 | 0 | 0 | + // 0 | 1 | 1 | 0 | 1 | + // 1 | 0 | 1 | 0 | 1 | + // 1 | 1 | 0 | 0 | 0 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + private SqlExpression ExpandNullableNotEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.NotEqual(left, right), + leftIsNull)); + + // !(?a) != b -> (a == b) || (a == null) + // + // a | b | F1 = a == b | F2 = (a == null) | F3 = F1 OR F2 | + // | | | | | + // 0 | 0 | 1 | 0 | 1 | + // 0 | 1 | 0 | 0 | 0 | + // 1 | 0 | 0 | 0 | 0 | + // 1 | 1 | 1 | 0 | 1 | + // N | 0 | N | 1 | 1 | + // N | 1 | N | 1 | 1 | + private SqlExpression ExpandNegatedNullableNotEqualNonNullable( + SqlExpression left, SqlExpression right, SqlExpression leftIsNull) + => SimplifyLogicalSqlBinaryExpression( + SqlExpressionFactory.OrElse( + SqlExpressionFactory.Equal(left, right), + leftIsNull)); + } +} diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs index 5610cf29666..1775dbbef18 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedQueryTranslationPostprocessor.cs @@ -1,27 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections; using System.Collections.Generic; -using System.Data.Common; -using System.Linq.Expressions; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query { - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// public class RelationalParameterBasedQueryTranslationPostprocessor { public RelationalParameterBasedQueryTranslationPostprocessor( @@ -38,7 +24,7 @@ public RelationalParameterBasedQueryTranslationPostprocessor( protected virtual bool UseRelationalNulls { get; } - public virtual (SelectExpression selectExpression, bool canCache) Optimize( + public virtual (SelectExpression, bool) Optimize( [NotNull] SelectExpression selectExpression, [NotNull] IReadOnlyDictionary parametersValues) { @@ -46,300 +32,24 @@ public virtual (SelectExpression selectExpression, bool canCache) Optimize( Check.NotNull(parametersValues, nameof(parametersValues)); var canCache = true; + var (sqlExpressionOptimized, optimizerCanCache) = new NullabilityBasedSqlProcessingExpressionVisitor( + Dependencies.SqlExpressionFactory, + parametersValues, + UseRelationalNulls).Process(selectExpression); - var inExpressionOptimized = new InExpressionValuesExpandingExpressionVisitor( - Dependencies.SqlExpressionFactory, parametersValues).Visit(selectExpression); - - if (!ReferenceEquals(selectExpression, inExpressionOptimized)) - { - canCache = false; - } - - var nullParametersOptimized = new ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( - Dependencies.SqlExpressionFactory, UseRelationalNulls, parametersValues).Visit(inExpressionOptimized); + canCache &= optimizerCanCache; var fromSqlParameterOptimized = new FromSqlParameterApplyingExpressionVisitor( Dependencies.SqlExpressionFactory, Dependencies.ParameterNameGeneratorFactory.Create(), - parametersValues).Visit(nullParametersOptimized); + parametersValues).Visit(sqlExpressionOptimized); - if (!ReferenceEquals(nullParametersOptimized, fromSqlParameterOptimized)) + if (!ReferenceEquals(sqlExpressionOptimized, fromSqlParameterOptimized)) { canCache = false; } - return (selectExpression: (SelectExpression)fromSqlParameterOptimized, canCache); - } - - private sealed class ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor : SqlExpressionOptimizingExpressionVisitor - { - private readonly IReadOnlyDictionary _parametersValues; - - public ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, - bool useRelationalNulls, - IReadOnlyDictionary parametersValues) - : base(sqlExpressionFactory, useRelationalNulls) - { - _parametersValues = parametersValues; - } - - protected override Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression) - { - var result = base.VisitSqlUnaryExpression(sqlUnaryExpression); - if (result is SqlUnaryExpression newUnaryExpression - && newUnaryExpression.Operand is SqlParameterExpression parameterOperand) - { - var parameterValue = _parametersValues[parameterOperand.Name]; - if (sqlUnaryExpression.OperatorType == ExpressionType.Equal) - { - return SqlExpressionFactory.Constant(parameterValue == null, sqlUnaryExpression.TypeMapping); - } - - if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual) - { - return SqlExpressionFactory.Constant(parameterValue != null, sqlUnaryExpression.TypeMapping); - } - } - - return result; - } - - protected override Expression VisitSqlBinaryExpression(SqlBinaryExpression sqlBinaryExpression) - { - var result = base.VisitSqlBinaryExpression(sqlBinaryExpression); - if (result is SqlBinaryExpression sqlBinaryResult) - { - var leftNullParameter = sqlBinaryResult.Left is SqlParameterExpression leftParameter - && _parametersValues[leftParameter.Name] == null; - - var rightNullParameter = sqlBinaryResult.Right is SqlParameterExpression rightParameter - && _parametersValues[rightParameter.Name] == null; - - if ((sqlBinaryResult.OperatorType == ExpressionType.Equal || sqlBinaryResult.OperatorType == ExpressionType.NotEqual) - && (leftNullParameter || rightNullParameter)) - { - return SimplifyNullComparisonExpression( - sqlBinaryResult.OperatorType, - sqlBinaryResult.Left, - sqlBinaryResult.Right, - leftNullParameter, - rightNullParameter, - sqlBinaryResult.TypeMapping); - } - } - - return result; - } - } - - private sealed class InExpressionValuesExpandingExpressionVisitor : ExpressionVisitor - { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly IReadOnlyDictionary _parametersValues; - - public InExpressionValuesExpandingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary parametersValues) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parametersValues = parametersValues; - } - - public override Expression Visit(Expression expression) - { - if (expression is InExpression inExpression - && inExpression.Values != null) - { - var inValues = new List(); - var hasNullValue = false; - RelationalTypeMapping typeMapping = null; - - switch (inExpression.Values) - { - case SqlConstantExpression sqlConstant: - { - typeMapping = sqlConstant.TypeMapping; - var values = (IEnumerable)sqlConstant.Value; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add(value); - } - - break; - } - - case SqlParameterExpression sqlParameter: - { - typeMapping = sqlParameter.TypeMapping; - var values = (IEnumerable)_parametersValues[sqlParameter.Name]; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add(value); - } - - break; - } - } - - var updatedInExpression = inValues.Count > 0 - ? _sqlExpressionFactory.In( - (SqlExpression)Visit(inExpression.Item), - _sqlExpressionFactory.Constant(inValues, typeMapping), - inExpression.IsNegated) - : null; - - var nullCheckExpression = hasNullValue - ? inExpression.IsNegated - ? _sqlExpressionFactory.IsNotNull(inExpression.Item) - : _sqlExpressionFactory.IsNull(inExpression.Item) - : null; - - if (updatedInExpression != null - && nullCheckExpression != null) - { - return inExpression.IsNegated - ? _sqlExpressionFactory.AndAlso(updatedInExpression, nullCheckExpression) - : _sqlExpressionFactory.OrElse(updatedInExpression, nullCheckExpression); - } - - if (updatedInExpression == null - && nullCheckExpression == null) - { - return _sqlExpressionFactory.Equal( - _sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(inExpression.IsNegated)); - } - - return (SqlExpression)updatedInExpression ?? nullCheckExpression; - } - - return base.Visit(expression); - } - } - - private sealed class FromSqlParameterApplyingExpressionVisitor : ExpressionVisitor - { - private readonly IDictionary _visitedFromSqlExpressions - = new Dictionary(ReferenceEqualityComparer.Instance); - - private readonly ISqlExpressionFactory _sqlExpressionFactory; - private readonly ParameterNameGenerator _parameterNameGenerator; - private readonly IReadOnlyDictionary _parametersValues; - - public FromSqlParameterApplyingExpressionVisitor( - ISqlExpressionFactory sqlExpressionFactory, - ParameterNameGenerator parameterNameGenerator, - IReadOnlyDictionary parametersValues) - { - _sqlExpressionFactory = sqlExpressionFactory; - _parameterNameGenerator = parameterNameGenerator; - _parametersValues = parametersValues; - } - - public override Expression Visit(Expression expression) - { - if (expression is FromSqlExpression fromSql) - { - if (!_visitedFromSqlExpressions.TryGetValue(fromSql, out var updatedFromSql)) - { - switch (fromSql.Arguments) - { - case ParameterExpression parameterExpression: - var parameterValues = (object[])_parametersValues[parameterExpression.Name]; - - var subParameters = new List(parameterValues.Length); - // ReSharper disable once ForCanBeConvertedToForeach - for (var i = 0; i < parameterValues.Length; i++) - { - var parameterName = _parameterNameGenerator.GenerateNext(); - if (parameterValues[i] is DbParameter dbParameter) - { - if (string.IsNullOrEmpty(dbParameter.ParameterName)) - { - dbParameter.ParameterName = parameterName; - } - else - { - parameterName = dbParameter.ParameterName; - } - - subParameters.Add(new RawRelationalParameter(parameterName, dbParameter)); - } - else - { - subParameters.Add( - new TypeMappedRelationalParameter( - parameterName, - parameterName, - _sqlExpressionFactory.GetTypeMappingForValue(parameterValues[i]), - parameterValues[i]?.GetType().IsNullableType())); - } - } - - updatedFromSql = new FromSqlExpression( - fromSql.Sql, - Expression.Constant( - new CompositeRelationalParameter( - parameterExpression.Name, - subParameters)), - fromSql.Alias); - - _visitedFromSqlExpressions[fromSql] = updatedFromSql; - break; - - case ConstantExpression constantExpression: - var existingValues = (object[])constantExpression.Value; - var constantValues = new object[existingValues.Length]; - for (var i = 0; i < existingValues.Length; i++) - { - var value = existingValues[i]; - if (value is DbParameter dbParameter) - { - var parameterName = _parameterNameGenerator.GenerateNext(); - if (string.IsNullOrEmpty(dbParameter.ParameterName)) - { - dbParameter.ParameterName = parameterName; - } - else - { - parameterName = dbParameter.ParameterName; - } - - constantValues[i] = new RawRelationalParameter(parameterName, dbParameter); - } - else - { - constantValues[i] = _sqlExpressionFactory.Constant( - value, _sqlExpressionFactory.GetTypeMappingForValue(value)); - } - } - - updatedFromSql = new FromSqlExpression( - fromSql.Sql, - Expression.Constant(constantValues, typeof(object[])), - fromSql.Alias); - - _visitedFromSqlExpressions[fromSql] = updatedFromSql; - break; - } - } - - return updatedFromSql; - } - - return base.Visit(expression); - } + return ((SelectExpression)fromSqlParameterOptimized, canCache); } } } diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs index d25ecdf8a09..766588bfaf8 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPostprocessor.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -11,8 +12,6 @@ namespace Microsoft.EntityFrameworkCore.Query { public class RelationalQueryTranslationPostprocessor : QueryTranslationPostprocessor { - private readonly SqlExpressionOptimizingExpressionVisitor _sqlExpressionOptimizingExpressionVisitor; - public RelationalQueryTranslationPostprocessor( [NotNull] QueryTranslationPostprocessorDependencies dependencies, [NotNull] RelationalQueryTranslationPostprocessorDependencies relationalDependencies, @@ -25,8 +24,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 +39,15 @@ 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); - } - +#pragma warning disable CS0618 // Type or member is obsolete query = OptimizeSqlExpression(query); +#pragma warning restore CS0618 // Type or member is obsolete return query; } + [Obsolete("Use 'Optimize' method on " + nameof(RelationalParameterBasedQueryTranslationPostprocessor) + " instead. If you have a case for optimizations to be performed here, please file an issue on github.com/aspnet/EntityFrameworkCore.")] 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/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 730176fa978..10760758cc8 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -74,7 +74,6 @@ public static IServiceCollection AddEntityFrameworkSqlServer([NotNull] this ISer .TryAdd() .TryAdd() .TryAdd() - .TryAdd() .TryAdd() .TryAdd() .TryAddProviderSpecificServices( diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index 685999152bc..a90cf2b6d58 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -23,24 +23,24 @@ public SearchConditionConvertingExpressionVisitor( _sqlExpressionFactory = sqlExpressionFactory; } - private Expression ApplyConversion(SqlExpression sqlExpression, bool condition) + private SqlExpression ApplyConversion(SqlExpression sqlExpression, bool condition) => _isSearchCondition ? ConvertToSearchCondition(sqlExpression, condition) : ConvertToValue(sqlExpression, condition); - private Expression ConvertToSearchCondition(SqlExpression sqlExpression, bool condition) + private SqlExpression ConvertToSearchCondition(SqlExpression sqlExpression, bool condition) => condition ? sqlExpression : BuildCompareToExpression(sqlExpression); - private Expression ConvertToValue(SqlExpression sqlExpression, bool condition) + private SqlExpression ConvertToValue(SqlExpression sqlExpression, bool condition) { return condition ? _sqlExpressionFactory.Case( new[] { new CaseWhenClause( - sqlExpression, + SimplifyNegatedBinary(sqlExpression), _sqlExpressionFactory.ApplyDefaultTypeMapping(_sqlExpressionFactory.Constant(true))) }, _sqlExpressionFactory.Constant(false)) @@ -48,9 +48,32 @@ private Expression ConvertToValue(SqlExpression sqlExpression, bool condition) } private SqlExpression BuildCompareToExpression(SqlExpression sqlExpression) - { - return _sqlExpressionFactory.Equal(sqlExpression, _sqlExpressionFactory.Constant(true)); - } + => sqlExpression is SqlConstantExpression sqlConstantExpression + && sqlConstantExpression.Value is bool boolValue + ? _sqlExpressionFactory.Equal( + boolValue + ? _sqlExpressionFactory.Constant(1) + : _sqlExpressionFactory.Constant(0), + _sqlExpressionFactory.Constant(1)) + : _sqlExpressionFactory.Equal( + sqlExpression, + _sqlExpressionFactory.Constant(true)); + + // !(a == b) -> (a != b) + // !(a != b) -> (a == b) + private SqlExpression SimplifyNegatedBinary(SqlExpression sqlExpression) + => sqlExpression is SqlUnaryExpression sqlUnaryExpression + && sqlUnaryExpression.IsLogicalNot() + && sqlUnaryExpression.Operand is SqlBinaryExpression sqlBinaryOperand + && (sqlBinaryOperand.OperatorType == ExpressionType.Equal || sqlBinaryOperand.OperatorType == ExpressionType.NotEqual) + ? _sqlExpressionFactory.MakeBinary( + sqlBinaryOperand.OperatorType == ExpressionType.Equal + ? ExpressionType.NotEqual + : ExpressionType.Equal, + sqlBinaryOperand.Left, + sqlBinaryOperand.Right, + sqlBinaryOperand.TypeMapping) + : sqlExpression; protected override Expression VisitCase(CaseExpression caseExpression) { @@ -273,9 +296,13 @@ when sqlUnaryExpression.IsLogicalNot(): } var operand = (SqlExpression)Visit(sqlUnaryExpression.Operand); + _isSearchCondition = parentSearchCondition; - return ApplyConversion(sqlUnaryExpression.Update(operand), condition: resultCondition); + return SimplifyNegatedBinary( + ApplyConversion( + sqlUnaryExpression.Update(operand), + condition: resultCondition)); } protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstantExpression) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs index b7cf63d1973..6f2f58771be 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedQueryTranslationPostprocessor.cs @@ -18,7 +18,7 @@ public SqlServerParameterBasedQueryTranslationPostprocessor( { } - public override (SelectExpression selectExpression, bool canCache) Optimize( + public override (SelectExpression, bool) Optimize( SelectExpression selectExpression, IReadOnlyDictionary parametersValues) { @@ -27,8 +27,8 @@ public override (SelectExpression selectExpression, bool canCache) Optimize( var (optimizedSelectExpression, canCache) = base.Optimize(selectExpression, parametersValues); - var searchConditionOptimized = (SelectExpression)new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory) - .Visit(optimizedSelectExpression); + var searchConditionOptimized = (SelectExpression)new SearchConditionConvertingExpressionVisitor( + Dependencies.SqlExpressionFactory).Visit(optimizedSelectExpression); return (searchConditionOptimized, canCache); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs index 5c8b9ac34bd..adbfd52d806 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindJoinQueryCosmosTest.cs @@ -492,6 +492,18 @@ public override Task GroupJoin_Subquery_with_Take_Then_SelectMany_Where(bool asy return base.GroupJoin_Subquery_with_Take_Then_SelectMany_Where(async); } + [ConditionalTheory(Skip = "Issue#17246")] + public override Task Inner_join_with_tautology_predicate_converts_to_cross_join(bool async) + { + return base.Inner_join_with_tautology_predicate_converts_to_cross_join(async); + } + + [ConditionalTheory(Skip = "Issue#17246")] + public override Task Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(bool async) + { + return base.Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs index 57f89a74ffa..e9bf5dca845 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NullSemanticsQueryTestBase.cs @@ -316,7 +316,7 @@ public virtual void Contains_with_local_array_closure_false_with_null() AssertQuery(es => es.Where(e => !ids.Contains(e.NullableStringA))); } - [ConditionalFact(Skip = "issue #14171")] + [ConditionalFact] public virtual void Contains_with_local_nullable_array_closure_negated() { string[] ids = { "Foo" }; @@ -946,40 +946,58 @@ join e2 in _clientData._entities2 } } - [ConditionalFact(Skip = "issue #14171")] + [ConditionalFact] public virtual void Null_semantics_contains() { - using var ctx = CreateContext(); var ids = new List { 1, 2 }; - var query1 = ctx.Entities1.Where(e => ids.Contains(e.NullableIntA)); - var result1 = query1.ToList(); + AssertQuery(es => es.Where(e => ids.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids.Contains(e.NullableIntA))); - var query2 = ctx.Entities1.Where(e => !ids.Contains(e.NullableIntA)); - var result2 = query2.ToList(); + var ids2 = new List { 1, 2, null }; + AssertQuery(es => es.Where(e => ids2.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids2.Contains(e.NullableIntA))); - var ids2 = new List - { - 1, - 2, - null - }; - var query3 = ctx.Entities1.Where(e => ids.Contains(e.NullableIntA)); - var result3 = query3.ToList(); + AssertQuery(es => es.Where(e => new List { 1, 2 }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List { 1, 2 }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => new List { 1, 2, null }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List { 1, 2, null }.Contains(e.NullableIntA))); + } - var query4 = ctx.Entities1.Where(e => !ids.Contains(e.NullableIntA)); - var result4 = query4.ToList(); + [ConditionalFact] + public virtual void Null_semantics_contains_array_with_no_values() + { + var ids = new List(); + AssertQuery(es => es.Where(e => ids.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids.Contains(e.NullableIntA))); - var query5 = ctx.Entities1.Where(e => !new List { 1, 2 }.Contains(e.NullableIntA)); - var result5 = query5.ToList(); + var ids2 = new List { null }; + AssertQuery(es => es.Where(e => ids2.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !ids2.Contains(e.NullableIntA))); - var query6 = ctx.Entities1.Where( - e => !new List - { - 1, - 2, - null - }.Contains(e.NullableIntA)); - var result6 = query6.ToList(); + AssertQuery(es => es.Where(e => new List().Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List().Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => new List { null }.Contains(e.NullableIntA))); + AssertQuery(es => es.Where(e => !new List { null }.Contains(e.NullableIntA))); + } + + [ConditionalFact] + public virtual void Null_semantics_contains_non_nullable_argument() + { + var ids = new List { 1, 2, null }; + AssertQuery(es => es.Where(e => ids.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids.Contains(e.IntA))); + + var ids2 = new List { 1, 2, }; + AssertQuery(es => es.Where(e => ids2.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids2.Contains(e.IntA))); + + var ids3 = new List(); + AssertQuery(es => es.Where(e => ids3.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids3.Contains(e.IntA))); + + var ids4 = new List { null }; + AssertQuery(es => es.Where(e => ids4.Contains(e.IntA))); + AssertQuery(es => es.Where(e => !ids4.Contains(e.IntA))); } [ConditionalFact] @@ -1021,6 +1039,23 @@ public virtual void Null_semantics_with_null_check_complex() var result3 = query3.ToList(); } + [ConditionalFact] + public virtual void Null_semantics_with_null_check_complex2() + { + using var ctx = CreateContext(); + var query1 = ctx.Entities1.Where(e => ((e.NullableBoolA != null) + && (e.NullableBoolB != null) + && ((e.NullableBoolB != e.NullableBoolA) || (e.NullableBoolC != null)) + && (e.NullableBoolC != e.NullableBoolB)) + || (e.NullableBoolC != e.BoolB)).ToList(); + + var query2 = ctx.Entities1.Where(e => ((e.NullableBoolA != null) + && (e.NullableBoolB != null) + && ((e.NullableBoolB != e.NullableBoolA) || (e.NullableBoolC != null)) + && (e.NullableBoolC != e.NullableBoolB)) + || (e.NullableBoolB != e.BoolB)).ToList(); + } + [ConditionalFact] public virtual void IsNull_on_complex_expression() { @@ -1044,6 +1079,110 @@ public virtual void Coalesce_not_equal() AssertQuery(es => es.Where(e => (e.NullableIntA ?? 0) != 0)); } + [ConditionalFact] + public virtual void Negated_order_comparison_on_non_nullable_arguments_gets_optimized() + { + var i = 1; + AssertQuery(es => es.Where(e => !(e.IntA > i))); + AssertQuery(es => es.Where(e => !(e.IntA >= i))); + AssertQuery(es => es.Where(e => !(e.IntA < i))); + AssertQuery(es => es.Where(e => !(e.IntA <= i))); + } + + [ConditionalFact(Skip = "issue #9544")] + public virtual void Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized() + { + var i = 1; + AssertQuery(es => es.Where(e => !(e.NullableIntA > i))); + AssertQuery(es => es.Where(e => !(e.NullableIntA >= i))); + AssertQuery(es => es.Where(e => !(e.NullableIntA < i))); + AssertQuery(es => es.Where(e => !(e.NullableIntA <= i))); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagates_inside_binary_AndAlso() + { + AssertQuery(es => es.Where(e => e.NullableStringA != null && e.NullableStringB != null && e.NullableStringA != e.NullableStringB)); + } + + [ConditionalFact] + public virtual void Nullable_column_info_doesnt_propagate_inside_binary_OrElse() + { + AssertQuery(es => es.Where(e => (e.NullableStringA != null || e.NullableStringB != null) && e.NullableStringA != e.NullableStringB)); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagates_inside_binary_OrElse_when_info_is_duplicated() + { + AssertQuery(es => es.Where(e => ((e.NullableStringA != null && e.NullableStringB != null) || (e.NullableStringA != null)) && e.NullableStringA != e.NullableStringB)); + AssertQuery(es => es.Where(e => ((e.NullableStringA != null && e.NullableStringB != null) || (e.NullableStringB != null && e.NullableStringA != null)) && e.NullableStringA != e.NullableStringB)); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagates_inside_conditional() + { + using var ctx = CreateContext(); + var query1 = ctx.Entities1.Select(e => e.NullableStringA != null ? e.NullableStringA != e.StringA : e.BoolA).ToList(); + } + + [ConditionalFact] + public virtual void Nullable_column_info_doesnt_propagate_between_projections() + { + using var ctx = CreateContext(); + var query = ctx.Entities1.Select(e => new { Foo = e.NullableStringA != null, Bar = e.NullableStringA != e.StringA }).ToList(); + } + + [ConditionalFact] + public virtual void Nullable_column_info_doesnt_propagate_between_different_parts_of_select() + { + AssertQuery(es => from e1 in es + join e2 in es on e1.NullableBoolA != null equals false + where e1.NullableBoolA != e2.NullableBoolB + select e1); + } + + [ConditionalFact] + public virtual void Nullable_column_info_propagation_complex() + { + AssertQuery(es => es.Where(e => (e.NullableStringA != null && e.NullableBoolB != null && e.NullableStringC != null) + && ((e.NullableStringA != null || e.NullableBoolC != null) + && e.NullableBoolB != e.NullableBoolC))); + } + + [ConditionalFact] + public virtual void String_concat_with_both_arguments_being_null() + { + var prm = default(string); + + using var ctx = CreateContext(); + var prmPrmQuery = ctx.Entities1.Select(x => prm + prm).ToList(); + var prmPrmExpected = ctx.Entities1.ToList().Select(x => prm + prm).ToList(); + + var prmConstQuery = ctx.Entities1.Select(x => prm + null).ToList(); + var prmConstExpected = ctx.Entities1.ToList().Select(x => prm + null).ToList(); + + var prmColumn = ctx.Entities1.Select(x => prm + x.NullableStringA).ToList(); + var prmColumnExpected = ctx.Entities1.ToList().Select(x => prm + x.NullableStringA).ToList(); + + var constPrmQuery = ctx.Entities1.Select(x => null + prm).ToList(); + var constPrmExpected = ctx.Entities1.ToList().Select(x => null + prm).ToList(); + + var constConstQuery = ctx.Entities1.Select(x => (string)null + null).ToList(); + var constConstExpected = ctx.Entities1.ToList().Select(x => (string)null + null).ToList(); + + var constColumn = ctx.Entities1.Select(x => null + x.NullableStringA).ToList(); + var constColumnExpected = ctx.Entities1.ToList().Select(x => null + x.NullableStringA).ToList(); + + var columnPrmQuery = ctx.Entities1.Select(x => x.NullableStringB + prm).ToList(); + var columnPrmExpected = ctx.Entities1.ToList().Select(x => x.NullableStringB + prm).ToList(); + + var columnConstQuery = ctx.Entities1.Select(x => x.NullableStringB + null).ToList(); + var columnConstExpected = ctx.Entities1.ToList().Select(x => x.NullableStringB + null).ToList(); + + var columnColumn = ctx.Entities1.Select(x => x.NullableStringB + x.NullableStringA).ToList(); + var columnColumnExpected = ctx.Entities1.ToList().Select(x => x.NullableStringB + x.NullableStringA).ToList(); + } + protected static TResult Maybe(object caller, Func expression) where TResult : class { diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 8aec7e573ae..09f4c09d9dc 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -4567,7 +4567,7 @@ public virtual Task Nav_rewrite_doesnt_apply_null_protection_for_function_argume .Select(l1 => Math.Max(l1.OneToOne_Optional_PK1.Level1_Required_Id, 7))); } - [ConditionalTheory(Skip = "See issue#11464")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Accessing_optional_property_inside_result_operator_subquery(bool async) { diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 77c52876b53..c3e03edc4ee 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -7164,7 +7164,7 @@ public virtual Task Where_null_parameter_is_not_null(bool async) ss => ss.Set().Where(g => false)); } - [ConditionalTheory(Skip = "issue #19019")] + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task OrderBy_StartsWith_with_null_parameter_as_argument(bool async) { @@ -7177,6 +7177,18 @@ public virtual Task OrderBy_StartsWith_with_null_parameter_as_argument(bool asyn assertOrder: true); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task OrderBy_Contains_empty_list(bool async) + { + var ids = new List(); + + return AssertQuery( + async, + ss => ss.Set().OrderBy(g => ids.Contains(g.SquadId)).Select(g => g), + ss => ss.Set().OrderBy(g => ids.Contains(g.SquadId)).Select(g => g)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Where_with_enum_flags_parameter(bool async) diff --git a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs index d1cee909566..ad066890ee9 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindJoinQueryTestBase.cs @@ -708,5 +708,28 @@ join o in ss.Set().OrderBy(o => o.OrderID).Take(100) on c.CustomerID equa from o in lo.Where(x => x.CustomerID.StartsWith("A")) select new { c.CustomerID, o.OrderID }); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Inner_join_with_tautology_predicate_converts_to_cross_join(bool async) + { + return AssertQuery( + async, + ss => from c in ss.Set().OrderBy(c => c.CustomerID).Take(10) + join o in ss.Set().OrderBy(o => o.OrderID).Take(10) on 1 equals 1 + select new { c.CustomerID, o.OrderID }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(bool async) + { + return AssertQuery( + async, + ss => from c in ss.Set().OrderBy(c => c.CustomerID).Take(10) + join o in ss.Set().OrderBy(o => o.OrderID).Take(10) on c.CustomerID != null equals true into grouping + from o in grouping.DefaultIfEmpty() + select new { c.CustomerID, o.OrderID }); + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs index fb2e41e13ab..fd115b48b52 100644 --- a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs @@ -538,7 +538,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK(EntityState state, bool async) @@ -548,7 +548,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_collection_not_found(EntityState state, bool async) @@ -1153,7 +1153,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_alternate_key(EntityState state, bool async) @@ -1163,7 +1163,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_collection_shadow_fk(EntityState state, bool async) @@ -1283,7 +1283,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(EntityState state, bool async) @@ -1293,7 +1293,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_collection_composite_key(EntityState state, bool async) @@ -1421,7 +1421,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_composite_key(EntityState state, bool async) @@ -1431,7 +1431,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu AssertSql( @"SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index ae2be9faa87..852fc64e496 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) @@ -3092,10 +3092,10 @@ public override async Task Select_optional_navigation_property_string_concat(boo await base.Select_optional_navigation_property_string_concat(async); AssertSql( - @"SELECT (COALESCE([l].[Name], N'') + N' ') + CASE + @"SELECT (COALESCE([l].[Name], N'') + N' ') + COALESCE(CASE WHEN [t].[Id] IS NOT NULL THEN [t].[Name] ELSE N'NULL' -END +END, N'') FROM [LevelOne] AS [l] LEFT JOIN ( SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] @@ -3300,9 +3300,10 @@ public override async Task Accessing_optional_property_inside_result_operator_su await base.Accessing_optional_property_inside_result_operator_subquery(async); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[OneToMany_Optional_Self_Inverse1Id], [l1].[OneToMany_Required_Self_Inverse1Id], [l1].[OneToOne_Optional_Self1Id], [l1.OneToOne_Optional_FK1].[Name] -FROM [LevelOne] AS [l1] -LEFT JOIN [LevelTwo] AS [l1.OneToOne_Optional_FK1] ON [l1].[Id] = [l1.OneToOne_Optional_FK1].[Level1_Optional_Id]"); + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id] +FROM [LevelOne] AS [l] +LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id] +WHERE [l0].[Name] NOT IN (N'Name1', N'Name2') OR [l0].[Name] IS NULL"); } public override async Task Include_after_SelectMany_and_reference_navigation(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs index 28611e2633e..7cd2674ad6f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FunkyDataQuerySqlServerTest.cs @@ -45,15 +45,15 @@ WHERE CHARINDEX(N'_Ba_', [f].[FirstName]) > 0", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(N'%B%a%r', [f].[FirstName]) <= 0", +WHERE NOT (CHARINDEX(N'%B%a%r', [f].[FirstName]) > 0)", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(NULL, [f].[FirstName]) <= 0"); +WHERE NOT (CHARINDEX(NULL, [f].[FirstName]) > 0)"); } public override async Task String_contains_on_argument_with_wildcard_parameter(bool async) @@ -73,11 +73,9 @@ SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] WHERE (@__prm2_0 = N'') OR (CHARINDEX(@__prm2_0, [f].[FirstName]) > 0)", // - @"@__prm3_0=NULL (Size = 4000) - -SELECT [f].[FirstName] + @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(@__prm3_0, [f].[FirstName]) > 0", +WHERE CHARINDEX(NULL, [f].[FirstName]) > 0", // @"@__prm4_0='' (Size = 4000) @@ -95,19 +93,17 @@ FROM [FunkyCustomers] AS [f] SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE (@__prm6_0 <> N'') AND (CHARINDEX(@__prm6_0, [f].[FirstName]) <= 0)", +WHERE NOT ((@__prm6_0 = N'') OR (CHARINDEX(@__prm6_0, [f].[FirstName]) > 0))", // @"@__prm7_0='' (Size = 4000) SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE (@__prm7_0 <> N'') AND (CHARINDEX(@__prm7_0, [f].[FirstName]) <= 0)", +WHERE NOT ((@__prm7_0 = N'') OR (CHARINDEX(@__prm7_0, [f].[FirstName]) > 0))", // - @"@__prm8_0=NULL (Size = 4000) - -SELECT [f].[FirstName] + @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CHARINDEX(@__prm8_0, [f].[FirstName]) <= 0"); +WHERE NOT (CHARINDEX(NULL, [f].[FirstName]) > 0)"); } public override async Task String_contains_on_argument_with_wildcard_column(bool async) @@ -129,7 +125,7 @@ public override async Task String_contains_on_argument_with_wildcard_column_nega @"SELECT [f].[FirstName] AS [fn], [f0].[LastName] AS [ln] FROM [FunkyCustomers] AS [f] CROSS JOIN [FunkyCustomers] AS [f0] -WHERE (([f0].[LastName] <> N'') OR [f0].[LastName] IS NULL) AND (CHARINDEX([f0].[LastName], [f].[FirstName]) <= 0)"); +WHERE NOT ((([f0].[LastName] = N'') AND [f0].[LastName] IS NOT NULL) OR (CHARINDEX([f0].[LastName], [f].[FirstName]) > 0))"); } public override async Task String_starts_with_on_argument_with_wildcard_constant(bool async) @@ -147,7 +143,7 @@ WHERE [f].[FirstName] IS NOT NULL AND ([f].[FirstName] LIKE N'a\_%' ESCAPE N'\') // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f]", @@ -162,11 +158,11 @@ WHERE [f].[FirstName] IS NOT NULL AND NOT ([f].[FirstName] LIKE N'\%B\%a\%r%' ES // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_starts_with_on_argument_with_wildcard_parameter(bool async) @@ -188,7 +184,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"@__prm4_0='' (Size = 4000) @@ -216,7 +212,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_starts_with_on_argument_with_bracket(bool async) @@ -296,7 +292,7 @@ WHERE [f].[FirstName] IS NOT NULL AND ([f].[FirstName] LIKE N'%a\_' ESCAPE N'\') // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f]", @@ -311,11 +307,11 @@ WHERE [f].[FirstName] IS NOT NULL AND NOT ([f].[FirstName] LIKE N'%\%B\%a\%r' ES // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_ends_with_on_argument_with_wildcard_parameter(bool async) @@ -337,7 +333,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"@__prm4_0='' (Size = 4000) @@ -365,7 +361,7 @@ FROM [FunkyCustomers] AS [f] // @"SELECT [f].[FirstName] FROM [FunkyCustomers] AS [f] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task String_ends_with_on_argument_with_wildcard_column(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index be1c0f8abf2..1c5d5e14bc5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -28,7 +28,7 @@ public override async Task Entity_equality_empty(bool async) AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Include_multiple_one_to_one_and_one_to_many(bool async) @@ -490,11 +490,9 @@ public override async Task Where_bitwise_and_nullable_enum_with_nullable_paramet FROM [Weapons] AS [w] WHERE ([w].[AmmunitionType] & @__ammunitionType_0) > 0", // - @"@__ammunitionType_0=NULL (DbType = Int32) - -SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] + @"SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] -WHERE ([w].[AmmunitionType] & @__ammunitionType_0) > 0"); +WHERE ([w].[AmmunitionType] & NULL) > 0"); } public override async Task Where_bitwise_or_enum(bool async) @@ -758,9 +756,7 @@ public override async Task Select_null_parameter(bool async) SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] FROM [Weapons] AS [w]", // - @"@__ammunitionType_0=NULL (DbType = Int32) - -SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] + @"SELECT [w].[Id], NULL AS [AmmoType] FROM [Weapons] AS [w]", // @"@__ammunitionType_0='2' (Nullable = true) @@ -768,9 +764,7 @@ public override async Task Select_null_parameter(bool async) SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] FROM [Weapons] AS [w]", // - @"@__ammunitionType_0=NULL (DbType = Int32) - -SELECT [w].[Id], @__ammunitionType_0 AS [AmmoType] + @"SELECT [w].[Id], NULL AS [AmmoType] FROM [Weapons] AS [w]"); } @@ -869,10 +863,7 @@ public override async Task Null_propagation_optimization1(bool async) AssertSql( @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE [g].[Discriminator] IN (N'Gear', N'Officer') AND (CASE - WHEN ([g].[LeaderNickname] = N'Marcus') AND [g].[LeaderNickname] IS NOT NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) -END = CAST(1 AS bit))"); +WHERE [g].[Discriminator] IN (N'Gear', N'Officer') AND (([g].[LeaderNickname] = N'Marcus') AND [g].[LeaderNickname] IS NOT NULL)"); } public override async Task Null_propagation_optimization2(bool async) @@ -1077,10 +1068,7 @@ public override async Task Select_null_propagation_negative7(bool async) AssertSql( @"SELECT CASE - WHEN [g].[LeaderNickname] IS NOT NULL THEN CASE - WHEN (([g].[LeaderNickname] = [g].[LeaderNickname]) AND [g].[LeaderNickname] IS NOT NULL) OR [g].[LeaderNickname] IS NULL THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) - END + WHEN [g].[LeaderNickname] IS NOT NULL THEN CAST(1 AS bit) ELSE NULL END FROM [Gears] AS [g] @@ -1224,7 +1212,7 @@ public override async Task Where_compare_anonymous_types_with_uncorrelated_membe AssertSql( @"SELECT [g].[Nickname] FROM [Gears] AS [g] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Select_Where_Navigation_Scalar_Equals_Navigation_Scalar(bool async) @@ -6253,7 +6241,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) @@ -6665,7 +6652,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]"); } @@ -7068,7 +7055,7 @@ public override async Task Group_by_with_having_StartsWith_with_null_parameter_a FROM [Gears] AS [g] WHERE [g].[Discriminator] IN (N'Gear', N'Officer') GROUP BY [g].[FullName] -HAVING CAST(0 AS bit) = CAST(1 AS bit)"); +HAVING 0 = 1"); } public override async Task Select_StartsWith_with_null_parameter_as_argument(bool async) @@ -7076,10 +7063,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')"); } @@ -7113,7 +7097,20 @@ public override async Task OrderBy_StartsWith_with_null_parameter_as_argument(bo await base.OrderBy_StartsWith_with_null_parameter_as_argument(async); AssertSql( - @""); + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Gear', N'Officer') +ORDER BY [g].[Nickname]"); + } + + public override async Task OrderBy_Contains_empty_list(bool async) + { + await base.OrderBy_Contains_empty_list(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] +FROM [Gears] AS [g] +WHERE [g].[Discriminator] IN (N'Gear', N'Officer')"); } public override async Task Where_with_enum_flags_parameter(bool async) @@ -7139,7 +7136,7 @@ WHERE [g].[Discriminator] IN (N'Gear', N'Officer') AND (([g].[Rank] | @__rank_0) // @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task FirstOrDefault_navigation_access_entity_equality_in_where_predicate_apply_peneding_selector(bool async) @@ -7354,10 +7351,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))"); } @@ -7382,10 +7376,7 @@ FROM [Weapons] AS [w] WHERE [w].[Id] = [g].[SquadId]) IS NOT NULL THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END - ELSE CASE - WHEN CAST(0 AS bit) = CAST(1 AS bit) THEN CAST(1 AS bit) - ELSE CAST(0 AS bit) - END + ELSE CAST(0 AS bit) END = CAST(1 AS bit))"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs index dd0f818fce9..84b1ad1de0b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/InheritanceSqlServerTest.cs @@ -497,7 +497,7 @@ FROM [Animal] AS [a] FROM [Animal] AS [a0] WHERE [a0].[Discriminator] = N'Eagle' ) AS [t] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override void Member_access_on_intermediate_type_works() diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs index 71fdcd5636e..3dee9123006 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindAggregateOperatorsQuerySqlServerTest.cs @@ -821,7 +821,7 @@ public override async Task Contains_with_local_list_closure_all_null(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Contains_with_local_list_inline(bool async) @@ -942,7 +942,7 @@ public override async Task Contains_with_local_collection_empty_closure(bool asy AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Contains_with_local_collection_empty_inline(bool async) @@ -1171,10 +1171,8 @@ public override void Contains_over_entityType_with_null_should_rewrite_to_identi base.Contains_over_entityType_with_null_should_rewrite_to_identity_equality(); AssertSql( - @"@__entity_equality_p_0_OrderID=NULL (DbType = Int32) - -SELECT CASE - WHEN @__entity_equality_p_0_OrderID IN ( + @"SELECT CASE + WHEN NULL IN ( SELECT [o].[OrderID] FROM [Orders] AS [o] WHERE [o].[CustomerID] = N'VINET' diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 7fd2e5eddc1..6969acb44eb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -39,7 +39,7 @@ public override async Task String_StartsWith_Identity(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_StartsWith_Column(bool async) @@ -49,7 +49,7 @@ public override async Task String_StartsWith_Column(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (LEFT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_StartsWith_MethodCall(bool async) @@ -79,7 +79,7 @@ public override async Task String_EndsWith_Identity(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_EndsWith_Column(bool async) @@ -89,7 +89,7 @@ public override async Task String_EndsWith_Column(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName])))"); +WHERE ([c].[ContactName] = N'') OR ([c].[ContactName] IS NOT NULL AND (RIGHT([c].[ContactName], LEN([c].[ContactName])) = [c].[ContactName]))"); } public override async Task String_EndsWith_MethodCall(bool async) @@ -1182,7 +1182,7 @@ public override async Task Indexof_with_emptystring(bool async) AssertSql( @"SELECT CASE - WHEN N'' = N'' THEN 0 + WHEN 1 = 1 THEN 0 ELSE CAST(CHARINDEX(N'', [c].[ContactName]) AS int) - 1 END FROM [Customers] AS [c] @@ -1412,7 +1412,7 @@ public override async Task Static_equals_int_compared_to_long(bool async) AssertSql( @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Projecting_Math_Truncate_and_ordering_by_it_twice(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 7114bb61aa8..b3f448957c8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -1675,7 +1675,7 @@ WHEN NOT EXISTS ( SELECT 1 FROM [Orders] AS [o] GROUP BY [o].[CustomerID] - HAVING CAST(0 AS bit) = CAST(1 AS bit)) THEN CAST(1 AS bit) + HAVING 0 = 1) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } @@ -1690,7 +1690,7 @@ WHEN NOT EXISTS ( SELECT 1 FROM [Orders] AS [o] GROUP BY [o].[CustomerID] - HAVING SUM([o].[OrderID]) < 0) THEN CAST(1 AS bit) + HAVING NOT (SUM([o].[OrderID]) >= 0)) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs index bffae36883f..4dd10e801c0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindJoinQuerySqlServerTest.cs @@ -209,11 +209,11 @@ public override async Task Join_complex_condition(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -INNER JOIN ( +CROSS JOIN ( SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] WHERE [o].[OrderID] < 10250 -) AS [t] ON CAST(1 AS bit) = CAST(1 AS bit) +) AS [t] WHERE [c].[CustomerID] = N'ALFKI'"); } @@ -469,6 +469,48 @@ WHERE [t].[CustomerID] IS NOT NULL AND ([t].[CustomerID] LIKE N'A%') ) AS [t0] ON [c].[CustomerID] = [t0].[CustomerID]"); } + public override async Task Inner_join_with_tautology_predicate_converts_to_cross_join(bool async) + { + await base.Inner_join_with_tautology_predicate_converts_to_cross_join(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t0].[OrderID] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CustomerID] +) AS [t] +CROSS JOIN ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[OrderID] +) AS [t0] +ORDER BY [t].[CustomerID]"); + } + + public override async Task Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(bool async) + { + await base.Left_join_with_tautology_predicate_doesnt_convert_to_cross_join(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[CustomerID], [t0].[OrderID] +FROM ( + SELECT TOP(@__p_0) [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Customers] AS [c] + ORDER BY [c].[CustomerID] +) AS [t] +LEFT JOIN ( + SELECT TOP(@__p_0) [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] + FROM [Orders] AS [o] + ORDER BY [o].[OrderID] +) AS [t0] ON 1 = 1 +ORDER BY [t].[CustomerID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index adebd9bf9e6..6d8bfd4f7db 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -234,7 +234,7 @@ public override async Task Entity_equality_null(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Entity_equality_null_composite_key(bool async) @@ -244,7 +244,7 @@ public override async Task Entity_equality_null_composite_key(bool async) AssertSql( @"SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] FROM [Order Details] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Entity_equality_not_null(bool async) @@ -307,7 +307,7 @@ public override async Task Entity_equality_through_include(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Entity_equality_orderby(bool async) @@ -1460,7 +1460,7 @@ public override async Task All_top_level_column(bool async) WHEN NOT EXISTS ( SELECT 1 FROM [Customers] AS [c] - WHERE (([c].[ContactName] <> N'') OR [c].[ContactName] IS NULL) AND ([c].[ContactName] IS NULL OR ([c].[ContactName] IS NULL OR ((LEFT([c].[ContactName], LEN([c].[ContactName])) <> [c].[ContactName]) OR LEFT([c].[ContactName], LEN([c].[ContactName])) IS NULL)))) THEN CAST(1 AS bit) + WHERE (([c].[ContactName] <> N'') OR [c].[ContactName] IS NULL) AND ([c].[ContactName] IS NULL OR ((LEFT([c].[ContactName], LEN([c].[ContactName])) <> [c].[ContactName]) OR LEFT([c].[ContactName], LEN([c].[ContactName])) IS NULL))) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } @@ -3035,7 +3035,7 @@ FROM [Orders] AS [o] // @"SELECT [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] FROM [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Parameter_extraction_short_circuits_3(bool async) @@ -4258,7 +4258,7 @@ public override async Task Comparing_different_entity_types_using_Equals(bool as @"SELECT [c].[CustomerID] FROM [Customers] AS [c] CROSS JOIN [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_entity_to_null_using_Equals(bool async) @@ -4308,7 +4308,7 @@ public override async Task Comparing_non_matching_entities_using_Equals(bool asy @"SELECT [c].[CustomerID] AS [Id1], [o].[OrderID] AS [Id2] FROM [Customers] AS [c] CROSS JOIN [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_non_matching_collection_navigations_using_Equals(bool async) @@ -4319,7 +4319,7 @@ public override async Task Comparing_non_matching_collection_navigations_using_E @"SELECT [c].[CustomerID] AS [Id1], [o].[OrderID] AS [Id2] FROM [Customers] AS [c] CROSS JOIN [Orders] AS [o] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_collection_navigation_to_null(bool async) @@ -4329,7 +4329,7 @@ public override async Task Comparing_collection_navigation_to_null(bool async) AssertSql( @"SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Comparing_collection_navigation_to_null_complex(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index bed41970d24..f988a714ed2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -1168,7 +1168,7 @@ public override async Task Select_with_complex_expression_that_can_be_funcletize AssertSql( @"SELECT CASE - WHEN N'' = N'' THEN 0 + WHEN 1 = 1 THEN 0 ELSE CAST(CHARINDEX(N'', [c].[ContactName]) AS int) - 1 END FROM [Customers] AS [c] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index 431b8c44d90..248630bf864 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -522,7 +522,7 @@ public override async Task Where_equals_using_object_overload_on_mismatched_type AssertSql( @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); // See issue#17498 //Assert.Contains( @@ -549,11 +549,11 @@ public override async Task Where_equals_on_mismatched_types_nullable_int_long(bo AssertSql( @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); // See issue#17498 //Assert.Contains( @@ -572,11 +572,11 @@ public override async Task Where_equals_on_mismatched_types_nullable_long_nullab AssertSql( @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [e].[EmployeeID], [e].[City], [e].[Country], [e].[FirstName], [e].[ReportsTo], [e].[Title] FROM [Employees] AS [e] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); // See issue#17498 //Assert.Contains( @@ -898,7 +898,7 @@ public override async Task Where_constant_is_null(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_is_not_null(bool async) @@ -918,7 +918,7 @@ public override async Task Where_null_is_not_null(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_constant_is_not_null(bool async) @@ -1121,10 +1121,10 @@ public override async Task Where_negated_boolean_expression_compared_to_another_ @"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] FROM [Products] AS [p] WHERE CASE - WHEN [p].[ProductID] > 50 THEN CAST(1 AS bit) + WHEN [p].[ProductID] <= 50 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END = CASE - WHEN [p].[ProductID] > 20 THEN CAST(1 AS bit) + WHEN [p].[ProductID] <= 20 THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END"); } @@ -1253,7 +1253,7 @@ public override async Task Where_false(bool async) AssertSql( @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_default(bool async) @@ -1343,7 +1343,7 @@ public override async Task Where_concat_string_string_comparison(bool async) SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE (COALESCE(@__i_0, N'') + [c].[CustomerID]) = [c].[CompanyName]"); +WHERE (@__i_0 + [c].[CustomerID]) = [c].[CompanyName]"); } public override async Task Where_string_concat_method_comparison(bool async) @@ -1355,7 +1355,7 @@ public override async Task Where_string_concat_method_comparison(bool async) SELECT [c].[CustomerID] FROM [Customers] AS [c] -WHERE (COALESCE(@__i_0, N'') + [c].[CustomerID]) = [c].[CompanyName]"); +WHERE (@__i_0 + [c].[CustomerID]) = [c].[CompanyName]"); } public override async Task Where_ternary_boolean_condition_true(bool async) @@ -1407,7 +1407,7 @@ public override async Task Where_ternary_boolean_condition_with_false_as_result_ AssertSql( @"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] FROM [Products] AS [p] -WHERE CAST(0 AS bit) = CAST(1 AS bit)"); +WHERE 0 = 1"); } public override async Task Where_compare_constructed_equal(bool async) @@ -1665,7 +1665,7 @@ public override async Task Where_is_conditional(bool async) @"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock] FROM [Products] AS [p] WHERE CASE - WHEN CAST(1 AS bit) = CAST(1 AS bit) THEN CAST(0 AS bit) + WHEN 1 = 1 THEN CAST(0 AS bit) ELSE CAST(1 AS bit) END = CAST(1 AS bit)"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs index 6b4f413188f..ef59c96a9fb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NullSemanticsQuerySqlServerTest.cs @@ -730,7 +730,9 @@ public override void Contains_with_local_nullable_array_closure_negated() base.Contains_with_local_nullable_array_closure_negated(); AssertSql( - @""); + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableStringA] NOT IN (N'Foo') OR [e].[NullableStringA] IS NULL"); } public override void Contains_with_local_array_closure_with_multiple_nulls() @@ -782,7 +784,7 @@ public override void Where_multiple_ands_with_nullable_parameter_and_constant() SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE (((([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL) AND [e].[NullableStringA] IS NOT NULL) AND [e].[NullableStringA] IS NOT NULL) AND (([e].[NullableStringA] <> @__prm3_2) OR [e].[NullableStringA] IS NULL)"); +WHERE ((([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL) AND [e].[NullableStringA] IS NOT NULL) AND ([e].[NullableStringA] <> @__prm3_2)"); } public override void Where_multiple_ands_with_nullable_parameter_and_constant_not_optimized() @@ -794,7 +796,7 @@ public override void Where_multiple_ands_with_nullable_parameter_and_constant_no SELECT [e].[Id] FROM [Entities1] AS [e] -WHERE ((([e].[NullableStringB] IS NOT NULL AND (([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL)) AND [e].[NullableStringA] IS NOT NULL) AND [e].[NullableStringA] IS NOT NULL) AND (([e].[NullableStringA] <> @__prm3_2) OR [e].[NullableStringA] IS NULL)"); +WHERE (([e].[NullableStringB] IS NOT NULL AND (([e].[NullableStringA] <> N'Foo') OR [e].[NullableStringA] IS NULL)) AND [e].[NullableStringA] IS NOT NULL) AND ([e].[NullableStringA] <> @__prm3_2)"); } public override void Where_coalesce() @@ -1460,7 +1462,109 @@ public override void Null_semantics_contains() base.Null_semantics_contains(); AssertSql( - @""); + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) AND [e].[NullableIntA] IS NOT NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IN (1, 2) OR [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] NOT IN (1, 2) AND [e].[NullableIntA] IS NOT NULL"); + } + + public override void Null_semantics_contains_array_with_no_values() + { + base.Null_semantics_contains_array_with_no_values(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NOT NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NULL", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[NullableIntA] IS NOT NULL"); + } + + public override void Null_semantics_contains_non_nullable_argument() + { + base.Null_semantics_contains_non_nullable_argument(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] NOT IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] NOT IN (1, 2)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE 0 = 1", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e]"); } public override void Null_semantics_with_null_check_simple() @@ -1507,6 +1611,20 @@ FROM [Entities1] AS [e] WHERE ([e].[NullableIntA] IS NOT NULL OR [e].[NullableIntB] IS NOT NULL) AND (([e].[NullableIntA] = [e].[NullableIntC]) OR ([e].[NullableIntA] IS NULL AND [e].[NullableIntC] IS NULL))"); } + public override void Null_semantics_with_null_check_complex2() + { + base.Null_semantics_with_null_check_complex2(); + + AssertSql( + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE ((([e].[NullableBoolA] IS NOT NULL AND [e].[NullableBoolB] IS NOT NULL) AND (([e].[NullableBoolB] <> [e].[NullableBoolA]) OR [e].[NullableBoolC] IS NOT NULL)) AND (([e].[NullableBoolC] <> [e].[NullableBoolB]) OR [e].[NullableBoolC] IS NULL)) OR (([e].[NullableBoolC] <> [e].[BoolB]) OR [e].[NullableBoolC] IS NULL)", + // + @"SELECT [e].[Id], [e].[BoolA], [e].[BoolB], [e].[BoolC], [e].[IntA], [e].[IntB], [e].[IntC], [e].[NullableBoolA], [e].[NullableBoolB], [e].[NullableBoolC], [e].[NullableIntA], [e].[NullableIntB], [e].[NullableIntC], [e].[NullableStringA], [e].[NullableStringB], [e].[NullableStringC], [e].[StringA], [e].[StringB], [e].[StringC] +FROM [Entities1] AS [e] +WHERE ((([e].[NullableBoolA] IS NOT NULL AND [e].[NullableBoolB] IS NOT NULL) AND (([e].[NullableBoolB] <> [e].[NullableBoolA]) OR [e].[NullableBoolC] IS NOT NULL)) AND (([e].[NullableBoolC] <> [e].[NullableBoolB]) OR [e].[NullableBoolC] IS NULL)) OR (([e].[NullableBoolB] <> [e].[BoolB]) OR [e].[NullableBoolB] IS NULL)"); + } + public override void IsNull_on_complex_expression() { base.IsNull_on_complex_expression(); @@ -1539,6 +1657,129 @@ FROM [Entities1] AS [e] WHERE COALESCE([e].[NullableIntA], 0) <> 0"); } + public override void Negated_order_comparison_on_non_nullable_arguments_gets_optimized() + { + base.Negated_order_comparison_on_non_nullable_arguments_gets_optimized(); + + AssertSql( + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] <= @__i_0", + // + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] < @__i_0", + // + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] >= @__i_0", + // + @"@__i_0='1' + +SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE [e].[IntA] > @__i_0"); + } + + public override void Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized() + { + base.Negated_order_comparison_on_nullable_arguments_doesnt_get_optimized(); + + AssertSql( + @""); + } + + public override void Nullable_column_info_propagates_inside_binary_AndAlso() + { + base.Nullable_column_info_propagates_inside_binary_AndAlso(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE ([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL) AND ([e].[NullableStringA] <> [e].[NullableStringB])"); + } + + public override void Nullable_column_info_doesnt_propagate_inside_binary_OrElse() + { + base.Nullable_column_info_doesnt_propagate_inside_binary_OrElse(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringB] IS NOT NULL) AND ((([e].[NullableStringA] <> [e].[NullableStringB]) OR ([e].[NullableStringA] IS NULL OR [e].[NullableStringB] IS NULL)) AND ([e].[NullableStringA] IS NOT NULL OR [e].[NullableStringB] IS NOT NULL))"); + } + + public override void Nullable_column_info_propagates_inside_binary_OrElse_when_info_is_duplicated() + { + base.Nullable_column_info_propagates_inside_binary_OrElse_when_info_is_duplicated(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE (([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL) OR [e].[NullableStringA] IS NOT NULL) AND (([e].[NullableStringA] <> [e].[NullableStringB]) OR [e].[NullableStringB] IS NULL)", + // + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE (([e].[NullableStringA] IS NOT NULL AND [e].[NullableStringB] IS NOT NULL) OR ([e].[NullableStringB] IS NOT NULL AND [e].[NullableStringA] IS NOT NULL)) AND ([e].[NullableStringA] <> [e].[NullableStringB])"); + } + + public override void Nullable_column_info_propagates_inside_conditional() + { + base.Nullable_column_info_propagates_inside_conditional(); + + AssertSql( + @"SELECT CASE + WHEN [e].[NullableStringA] IS NOT NULL THEN CASE + WHEN [e].[NullableStringA] <> [e].[StringA] THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) + END + ELSE [e].[BoolA] +END +FROM [Entities1] AS [e]"); + } + + public override void Nullable_column_info_doesnt_propagate_between_projections() + { + base.Nullable_column_info_doesnt_propagate_between_projections(); + + AssertSql( + @"SELECT CASE + WHEN [e].[NullableStringA] IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Foo], CASE + WHEN ([e].[NullableStringA] <> [e].[StringA]) OR [e].[NullableStringA] IS NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END AS [Bar] +FROM [Entities1] AS [e]"); + } + + public override void Nullable_column_info_doesnt_propagate_between_different_parts_of_select() + { + base.Nullable_column_info_doesnt_propagate_between_different_parts_of_select(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +INNER JOIN [Entities1] AS [e0] ON [e].[NullableBoolA] IS NULL +WHERE (([e].[NullableBoolA] <> [e0].[NullableBoolB]) OR ([e].[NullableBoolA] IS NULL OR [e0].[NullableBoolB] IS NULL)) AND ([e].[NullableBoolA] IS NOT NULL OR [e0].[NullableBoolB] IS NOT NULL)"); + } + + public override void Nullable_column_info_propagation_complex() + { + base.Nullable_column_info_propagation_complex(); + + AssertSql( + @"SELECT [e].[Id] +FROM [Entities1] AS [e] +WHERE (([e].[NullableStringA] IS NOT NULL AND [e].[NullableBoolB] IS NOT NULL) AND [e].[NullableStringC] IS NOT NULL) AND (([e].[NullableBoolB] <> [e].[NullableBoolC]) OR [e].[NullableBoolC] IS NULL)"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index c1d71192a59..0fc0173bf59 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -97,7 +97,7 @@ FROM [OwnedPerson] AS [o15] WHERE [o13].[LeafAAddress_Country_PlanetId] IS NOT NULL ) AS [t14] ON [t11].[Id] = [t14].[Id] LEFT JOIN [Order] AS [o16] ON [o].[Id] = [o16].[ClientId] -WHERE CAST(0 AS bit) = CAST(1 AS bit) +WHERE 0 = 1 ORDER BY [o].[Id], [t].[Id], [o16].[ClientId], [o16].[Id]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs index 85e5315a9d6..95a6cc94a07 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs @@ -86,7 +86,7 @@ public override void DbContext_list_is_parameterized() AssertSql( @"SELECT [l].[Id], [l].[Tenant] FROM [ListFilter] AS [l] -WHERE CAST(0 AS bit) = CAST(1 AS bit)", +WHERE 0 = 1", // @"SELECT [l].[Id], [l].[Tenant] FROM [ListFilter] AS [l] diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index ba1de47a32d..5afafc9b3b0 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -132,7 +132,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) @@ -142,7 +142,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) @@ -172,7 +172,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) @@ -182,7 +182,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) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs index ac3a1b778d8..4c7de105b4f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs @@ -155,7 +155,7 @@ public override async Task Select_expression_date_add_milliseconds_large_number_ AssertSql( @"@__millisecondsPerDay_0='86400000' (DbType = String) -SELECT rtrim(rtrim(strftime('%Y-%m-%d %H:%M:%f', ""o"".""OrderDate"", CAST(CAST((CAST(((CAST(strftime('%f', ""o"".""OrderDate"") AS REAL) * 1000.0) % 1000.0) AS INTEGER) / @__millisecondsPerDay_0) AS REAL) AS TEXT) || ' days', CAST((CAST((CAST(((CAST(strftime('%f', ""o"".""OrderDate"") AS REAL) * 1000.0) % 1000.0) AS INTEGER) % @__millisecondsPerDay_0) AS REAL) / 1000.0) AS TEXT) || ' seconds'), '0'), '.') AS ""OrderDate"" +SELECT rtrim(rtrim(strftime('%Y-%m-%d %H:%M:%f', ""o"".""OrderDate"", COALESCE(CAST(CAST((CAST(((CAST(strftime('%f', ""o"".""OrderDate"") AS REAL) * 1000.0) % 1000.0) AS INTEGER) / @__millisecondsPerDay_0) AS REAL) AS TEXT), '') || ' days', COALESCE(CAST((CAST((CAST(((CAST(strftime('%f', ""o"".""OrderDate"") AS REAL) * 1000.0) % 1000.0) AS INTEGER) % @__millisecondsPerDay_0) AS REAL) / 1000.0) AS TEXT), '') || ' seconds'), '0'), '.') AS ""OrderDate"" FROM ""Orders"" AS ""o"" WHERE ""o"".""OrderDate"" IS NOT NULL"); }