diff --git a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs index ccfc99a3c8e..8e0cf8114bf 100644 --- a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs @@ -37,28 +37,28 @@ public interface ISqlExpressionFactory SqlExpression? ApplyDefaultTypeMapping(SqlExpression? sqlExpression); /// - /// Creates a new with the given arguments. + /// Creates a new with the given arguments. /// /// An representing SQL unary operator. /// A to apply unary operator on. /// The type of the created expression. /// A type mapping to be assigned to the created expression. - /// A with the given arguments. - SqlUnaryExpression? MakeUnary( + /// A with the given arguments. + SqlExpression? MakeUnary( ExpressionType operatorType, SqlExpression operand, Type type, RelationalTypeMapping? typeMapping = null); /// - /// Creates a new with the given arguments. + /// Creates a new with the given arguments. /// /// An representing SQL unary operator. /// The left operand of binary operation. /// The right operand of binary operation. /// A type mapping to be assigned to the created expression. - /// A with the given arguments. - SqlBinaryExpression? MakeBinary( + /// A with the given arguments. + SqlExpression? MakeBinary( ExpressionType operatorType, SqlExpression left, SqlExpression right, @@ -66,208 +66,208 @@ public interface ISqlExpressionFactory // Comparison /// - /// Creates a which represents an equality comparison. + /// Creates a which represents an equality comparison. /// /// The left operand. /// The right operand. /// An expression representing a SQL equality comparison. - SqlBinaryExpression Equal(SqlExpression left, SqlExpression right); + SqlExpression Equal(SqlExpression left, SqlExpression right); /// - /// Creates a which represents an inequality comparison. + /// Creates a which represents an inequality comparison. /// /// The left operand. /// The right operand. /// An expression representing a SQL inequality comparison. - SqlBinaryExpression NotEqual(SqlExpression left, SqlExpression right); + SqlExpression NotEqual(SqlExpression left, SqlExpression right); /// - /// Creates a which represents a greater than comparison. + /// Creates a which represents a greater than comparison. /// /// The left operand. /// The right operand. /// An expression representing a SQL greater than comparison. - SqlBinaryExpression GreaterThan(SqlExpression left, SqlExpression right); + SqlExpression GreaterThan(SqlExpression left, SqlExpression right); /// - /// Creates a which represents a greater than or equal comparison. + /// Creates a which represents a greater than or equal comparison. /// /// The left operand. /// The right operand. /// An expression representing a SQL greater than or equal comparison. - SqlBinaryExpression GreaterThanOrEqual(SqlExpression left, SqlExpression right); + SqlExpression GreaterThanOrEqual(SqlExpression left, SqlExpression right); /// - /// Creates a which represents a less than comparison. + /// Creates a which represents a less than comparison. /// /// The left operand. /// The right operand. /// An expression representing a SQL less than comparison. - SqlBinaryExpression LessThan(SqlExpression left, SqlExpression right); + SqlExpression LessThan(SqlExpression left, SqlExpression right); /// - /// Creates a which represents a less than or equal comparison. + /// Creates a which represents a less than or equal comparison. /// /// The left operand. /// The right operand. /// An expression representing a SQL less than or equal comparison. - SqlBinaryExpression LessThanOrEqual(SqlExpression left, SqlExpression right); + SqlExpression LessThanOrEqual(SqlExpression left, SqlExpression right); // Logical /// - /// Creates a which represents a logical AND operation. + /// Creates a which represents a logical AND operation. /// /// The left operand. /// The right operand. /// An expression representing a SQL AND operation. - SqlBinaryExpression AndAlso(SqlExpression left, SqlExpression right); + SqlExpression AndAlso(SqlExpression left, SqlExpression right); /// - /// Creates a which represents a logical OR operation. + /// Creates a which represents a logical OR operation. /// /// The left operand. /// The right operand. /// An expression representing a SQL OR operation. - SqlBinaryExpression OrElse(SqlExpression left, SqlExpression right); + SqlExpression OrElse(SqlExpression left, SqlExpression right); // Arithmetic /// - /// Creates a which represents an addition. + /// Creates a which represents an addition. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL addition. - SqlBinaryExpression Add( + SqlExpression Add( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); /// - /// Creates a which represents a subtraction. + /// Creates a which represents a subtraction. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL subtraction. - SqlBinaryExpression Subtract( + SqlExpression Subtract( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); /// - /// Creates a which represents a multiplication. + /// Creates a which represents a multiplication. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL multiplication. - SqlBinaryExpression Multiply( + SqlExpression Multiply( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); /// - /// Creates a which represents a division. + /// Creates a which represents a division. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL division. - SqlBinaryExpression Divide( + SqlExpression Divide( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); /// - /// Creates a which represents a modulo operation. + /// Creates a which represents a modulo operation. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL modulo operation. - SqlBinaryExpression Modulo( + SqlExpression Modulo( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); // Bitwise /// - /// Creates a which represents a bitwise AND operation. + /// Creates a which represents a bitwise AND operation. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL bitwise AND operation. - SqlBinaryExpression And( + SqlExpression And( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); /// - /// Creates a which represents a bitwise OR operation. + /// Creates a which represents a bitwise OR operation. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL bitwise OR operation. - SqlBinaryExpression Or( + SqlExpression Or( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); // Other /// - /// Creates a which represents a COALESCE operation. + /// Creates a which represents a COALESCE operation. /// /// The left operand. /// The right operand. /// A type mapping to be assigned to the created expression. /// An expression representing a SQL COALESCE operation. - SqlFunctionExpression Coalesce( + SqlExpression Coalesce( SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represent equality to null. + /// Creates a new which represent equality to null. /// /// A to compare to null. /// An expression representing IS NULL construct in a SQL tree. - SqlUnaryExpression IsNull(SqlExpression operand); + SqlExpression IsNull(SqlExpression operand); /// - /// Creates a new which represent inequality to null. + /// Creates a new which represent inequality to null. /// /// A to compare to non null. /// An expression representing IS NOT NULL construct in a SQL tree. - SqlUnaryExpression IsNotNull(SqlExpression operand); + SqlExpression IsNotNull(SqlExpression operand); /// - /// Creates a new which represent casting a SQL expression to different type. + /// Creates a new which represent casting a SQL expression to different type. /// /// A to cast. /// The return type of the expression after cast. /// A relational type mapping to use for conversion. /// An expression representing cast operation in a SQL tree. - SqlUnaryExpression Convert( + SqlExpression Convert( SqlExpression operand, Type type, RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represent a NOT operation in a SQL tree. + /// Creates a new which represent a NOT operation in a SQL tree. /// /// A to apply NOT on. /// An expression representing a NOT operation in a SQL tree. - SqlUnaryExpression Not(SqlExpression operand); + SqlExpression Not(SqlExpression operand); /// - /// Creates a new which represent a negation operation in a SQL tree. + /// Creates a new which represent a negation operation in a SQL tree. /// /// A to apply NOT on. /// An expression representing a negation operation in a SQL tree. - SqlUnaryExpression Negate(SqlExpression operand); + SqlExpression Negate(SqlExpression operand); /// /// Creates a new which represent a CASE statement in a SQL tree. @@ -276,7 +276,7 @@ SqlUnaryExpression Convert( /// A list of to compare and get result from. /// A value to return if no matches, if any. /// An expression representing a CASE statement in a SQL tree. - CaseExpression Case( + SqlExpression Case( SqlExpression operand, IReadOnlyList whenClauses, SqlExpression? elseResult); @@ -287,10 +287,10 @@ CaseExpression Case( /// A list of to evaluate condition and get result from. /// A value to return if no matches, if any. /// An expression representing a CASE statement in a SQL tree. - CaseExpression Case(IReadOnlyList whenClauses, SqlExpression? elseResult); + SqlExpression Case(IReadOnlyList whenClauses, SqlExpression? elseResult); /// - /// Creates a new which represents a function call in a SQL tree. + /// Creates a new which represents a function call in a SQL tree. /// /// The name of the function. /// The arguments of the function. @@ -299,7 +299,7 @@ CaseExpression Case( /// The of the expression. /// The associated with the expression. /// An expression representing a function call in a SQL tree. - SqlFunctionExpression Function( + SqlExpression Function( string name, IEnumerable arguments, bool nullable, @@ -308,7 +308,7 @@ SqlFunctionExpression Function( RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represents a function call in a SQL tree. + /// Creates a new which represents a function call in a SQL tree. /// /// The schema in which the function is defined. /// The name of the function. @@ -318,7 +318,7 @@ SqlFunctionExpression Function( /// The of the expression. /// The associated with the expression. /// An expression representing a function call in a SQL tree. - SqlFunctionExpression Function( + SqlExpression Function( string? schema, string name, IEnumerable arguments, @@ -328,7 +328,7 @@ SqlFunctionExpression Function( RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represents a function call in a SQL tree. + /// Creates a new which represents a function call in a SQL tree. /// /// An expression on which the function is applied. /// The name of the function. @@ -339,7 +339,7 @@ SqlFunctionExpression Function( /// The of the expression. /// The associated with the expression. /// An expression representing a function call in a SQL tree. - SqlFunctionExpression Function( + SqlExpression Function( SqlExpression instance, string name, IEnumerable arguments, @@ -350,21 +350,21 @@ SqlFunctionExpression Function( RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represents a niladic function call in a SQL tree. + /// Creates a new which represents a niladic function call in a SQL tree. /// /// The name of the function. /// A bool value indicating whether this function can return null. /// The of the expression. /// The associated with the expression. /// An expression representing a function call in a SQL tree. - SqlFunctionExpression NiladicFunction( + SqlExpression NiladicFunction( string name, bool nullable, Type returnType, RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represents a niladic function call in a SQL tree. + /// Creates a new which represents a niladic function call in a SQL tree. /// /// The schema in which the function is defined. /// The name of the function. @@ -372,7 +372,7 @@ SqlFunctionExpression NiladicFunction( /// The of the expression. /// The associated with the expression. /// An expression representing a function call in a SQL tree. - SqlFunctionExpression NiladicFunction( + SqlExpression NiladicFunction( string schema, string name, bool nullable, @@ -380,7 +380,7 @@ SqlFunctionExpression NiladicFunction( RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represents a niladic function call in a SQL tree. + /// Creates a new which represents a niladic function call in a SQL tree. /// /// An expression on which the function is applied. /// The name of the function. @@ -389,7 +389,7 @@ SqlFunctionExpression NiladicFunction( /// The of the expression. /// The associated with the expression. /// An expression representing a function call in a SQL tree. - SqlFunctionExpression NiladicFunction( + SqlExpression NiladicFunction( SqlExpression instance, string name, bool nullable, @@ -402,7 +402,7 @@ SqlFunctionExpression NiladicFunction( /// /// A subquery to check existence of. /// An expression representing an EXISTS operation in a SQL tree. - ExistsExpression Exists(SelectExpression subquery); + SqlExpression Exists(SelectExpression subquery); /// /// Creates a new which represents an IN operation in a SQL tree. @@ -410,7 +410,7 @@ SqlFunctionExpression NiladicFunction( /// An item to look into values. /// A subquery in which item is searched. /// An expression representing an IN operation in a SQL tree. - InExpression In(SqlExpression item, SelectExpression subquery); + SqlExpression In(SqlExpression item, SelectExpression subquery); /// /// Creates a new which represents an IN operation in a SQL tree. @@ -418,7 +418,7 @@ SqlFunctionExpression NiladicFunction( /// An item to look into values. /// A list of values in which item is searched. /// An expression representing an IN operation in a SQL tree. - InExpression In(SqlExpression item, IReadOnlyList values); + SqlExpression In(SqlExpression item, IReadOnlyList values); /// /// Creates a new which represents an IN operation in a SQL tree. @@ -426,7 +426,7 @@ SqlFunctionExpression NiladicFunction( /// An item to look into values. /// A parameterized list of values in which the item is searched. /// An expression representing an IN operation in a SQL tree. - InExpression In(SqlExpression item, SqlParameterExpression valuesParameter); + SqlExpression In(SqlExpression item, SqlParameterExpression valuesParameter); /// /// Creates a new which represents a LIKE in a SQL tree. @@ -435,31 +435,31 @@ SqlFunctionExpression NiladicFunction( /// A pattern to search. /// An optional escape character to use in LIKE. /// An expression representing a LIKE in a SQL tree. - LikeExpression Like(SqlExpression match, SqlExpression pattern, SqlExpression? escapeChar = null); + SqlExpression Like(SqlExpression match, SqlExpression pattern, SqlExpression? escapeChar = null); /// - /// Creates a new which represents a constant in a SQL tree. + /// Creates a new which represents a constant in a SQL tree. /// /// A value. /// The associated with the expression. /// An expression representing a constant in a SQL tree. - SqlConstantExpression Constant(object value, RelationalTypeMapping? typeMapping = null); + SqlExpression Constant(object value, RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represents a constant in a SQL tree. + /// Creates a new which represents a constant in a SQL tree. /// /// A value. /// The type for the constant. Useful when value is null. /// The associated with the expression. /// An expression representing a constant in a SQL tree. - SqlConstantExpression Constant(object? value, Type type, RelationalTypeMapping? typeMapping = null); + SqlExpression Constant(object? value, Type type, RelationalTypeMapping? typeMapping = null); /// - /// Creates a new which represents a SQL token. + /// Creates a new which represents a SQL token. /// /// A string token to print in SQL tree. /// An expression representing a SQL token. - SqlFragmentExpression Fragment(string sql); + SqlExpression Fragment(string sql); /// /// Attempts to creates a new expression that returns the smallest value from a list of expressions, e.g. an invocation of the diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 0a7ae6228eb..eff6de9605c 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -390,7 +390,7 @@ private SqlExpression ApplyTypeMappingOnJsonScalar( } /// - public virtual SqlBinaryExpression? MakeBinary( + public virtual SqlExpression? MakeBinary( ExpressionType operatorType, SqlExpression left, SqlExpression right, @@ -416,125 +416,131 @@ private SqlExpression ApplyTypeMappingOnJsonScalar( break; } - return (SqlBinaryExpression)ApplyTypeMapping( + return ApplyTypeMapping( new SqlBinaryExpression(operatorType, left, right, returnType, null), typeMapping); } /// - public virtual SqlBinaryExpression Equal(SqlExpression left, SqlExpression right) + public virtual SqlExpression Equal(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.Equal, left, right, null)!; /// - public virtual SqlBinaryExpression NotEqual(SqlExpression left, SqlExpression right) + public virtual SqlExpression NotEqual(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.NotEqual, left, right, null)!; /// - public virtual SqlBinaryExpression GreaterThan(SqlExpression left, SqlExpression right) + public virtual SqlExpression GreaterThan(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.GreaterThan, left, right, null)!; /// - public virtual SqlBinaryExpression GreaterThanOrEqual(SqlExpression left, SqlExpression right) + public virtual SqlExpression GreaterThanOrEqual(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.GreaterThanOrEqual, left, right, null)!; /// - public virtual SqlBinaryExpression LessThan(SqlExpression left, SqlExpression right) + public virtual SqlExpression LessThan(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.LessThan, left, right, null)!; /// - public virtual SqlBinaryExpression LessThanOrEqual(SqlExpression left, SqlExpression right) + public virtual SqlExpression LessThanOrEqual(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.LessThanOrEqual, left, right, null)!; /// - public virtual SqlBinaryExpression AndAlso(SqlExpression left, SqlExpression right) + public virtual SqlExpression AndAlso(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.AndAlso, left, right, null)!; /// - public virtual SqlBinaryExpression OrElse(SqlExpression left, SqlExpression right) + public virtual SqlExpression OrElse(SqlExpression left, SqlExpression right) => MakeBinary(ExpressionType.OrElse, left, right, null)!; /// - public virtual SqlBinaryExpression Add(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Add(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Add, left, right, typeMapping)!; /// - public virtual SqlBinaryExpression Subtract(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Subtract(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Subtract, left, right, typeMapping)!; /// - public virtual SqlBinaryExpression Multiply(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Multiply(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Multiply, left, right, typeMapping)!; /// - public virtual SqlBinaryExpression Divide(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Divide(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Divide, left, right, typeMapping)!; /// - public virtual SqlBinaryExpression Modulo(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Modulo(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Modulo, left, right, typeMapping)!; /// - public virtual SqlBinaryExpression And(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression And(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.And, left, right, typeMapping)!; /// - public virtual SqlBinaryExpression Or(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Or(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Or, left, right, typeMapping)!; /// - public virtual SqlFunctionExpression Coalesce(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Coalesce(SqlExpression left, SqlExpression right, RelationalTypeMapping? typeMapping = null) { var resultType = right.Type; var inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right) ?? _typeMappingSource.FindMapping(resultType, Dependencies.Model); - var typeMappedArguments = new List + left = ApplyTypeMapping(left, inferredTypeMapping); + right = ApplyTypeMapping(right, inferredTypeMapping); + + return left switch { - ApplyTypeMapping(left, inferredTypeMapping), ApplyTypeMapping(right, inferredTypeMapping) + SqlConstantExpression { Value: null } => right, + + SqlConstantExpression { Value: not null } or + ColumnExpression { IsNullable: false } => left, + + _ => new SqlFunctionExpression( + "COALESCE", + [left, right], + nullable: true, + // COALESCE is handled separately since it's only nullable if *all* arguments are null + argumentsPropagateNullability: [false, false], + resultType, + inferredTypeMapping) }; - - return new SqlFunctionExpression( - "COALESCE", - typeMappedArguments, - nullable: true, - // COALESCE is handled separately since it's only nullable if *all* arguments are null - argumentsPropagateNullability: [false, false], - resultType, - inferredTypeMapping); } /// - public virtual SqlUnaryExpression? MakeUnary( + public virtual SqlExpression? MakeUnary( ExpressionType operatorType, SqlExpression operand, Type type, RelationalTypeMapping? typeMapping = null) => SqlUnaryExpression.IsValidOperator(operatorType) - ? (SqlUnaryExpression)ApplyTypeMapping(new SqlUnaryExpression(operatorType, operand, type, null), typeMapping) + ? ApplyTypeMapping(new SqlUnaryExpression(operatorType, operand, type, null), typeMapping) : null; /// - public virtual SqlUnaryExpression IsNull(SqlExpression operand) + public virtual SqlExpression IsNull(SqlExpression operand) => MakeUnary(ExpressionType.Equal, operand, typeof(bool))!; /// - public virtual SqlUnaryExpression IsNotNull(SqlExpression operand) + public virtual SqlExpression IsNotNull(SqlExpression operand) => MakeUnary(ExpressionType.NotEqual, operand, typeof(bool))!; /// - public virtual SqlUnaryExpression Convert(SqlExpression operand, Type type, RelationalTypeMapping? typeMapping = null) + public virtual SqlExpression Convert(SqlExpression operand, Type type, RelationalTypeMapping? typeMapping = null) => MakeUnary(ExpressionType.Convert, operand, type.UnwrapNullableType(), typeMapping)!; /// - public virtual SqlUnaryExpression Not(SqlExpression operand) + public virtual SqlExpression Not(SqlExpression operand) => MakeUnary(ExpressionType.Not, operand, operand.Type, operand.TypeMapping)!; /// - public virtual SqlUnaryExpression Negate(SqlExpression operand) + public virtual SqlExpression Negate(SqlExpression operand) => MakeUnary(ExpressionType.Negate, operand, operand.Type, operand.TypeMapping)!; /// - public virtual CaseExpression Case(SqlExpression? operand, IReadOnlyList whenClauses, SqlExpression? elseResult) + public virtual SqlExpression Case(SqlExpression? operand, IReadOnlyList whenClauses, SqlExpression? elseResult) { var operandTypeMapping = operand!.TypeMapping ?? whenClauses.Select(wc => wc.Test.TypeMapping).FirstOrDefault(t => t != null) @@ -563,7 +569,7 @@ public virtual CaseExpression Case(SqlExpression? operand, IReadOnlyList - public virtual CaseExpression Case(IReadOnlyList whenClauses, SqlExpression? elseResult) + public virtual SqlExpression Case(IReadOnlyList whenClauses, SqlExpression? elseResult) { var resultTypeMapping = elseResult?.TypeMapping ?? whenClauses.Select(wc => wc.Result.TypeMapping).FirstOrDefault(t => t != null); @@ -583,7 +589,7 @@ public virtual CaseExpression Case(IReadOnlyList whenClauses, Sq } /// - public virtual SqlFunctionExpression Function( + public virtual SqlExpression Function( string name, IEnumerable arguments, bool nullable, @@ -602,7 +608,7 @@ public virtual SqlFunctionExpression Function( } /// - public virtual SqlFunctionExpression Function( + public virtual SqlExpression Function( string? schema, string name, IEnumerable arguments, @@ -622,7 +628,7 @@ public virtual SqlFunctionExpression Function( } /// - public virtual SqlFunctionExpression Function( + public virtual SqlExpression Function( SqlExpression instance, string name, IEnumerable arguments, @@ -645,64 +651,64 @@ public virtual SqlFunctionExpression Function( } /// - public virtual SqlFunctionExpression NiladicFunction( + public virtual SqlExpression NiladicFunction( string name, bool nullable, Type returnType, RelationalTypeMapping? typeMapping = null) - => new(name, nullable, returnType, typeMapping); + => new SqlFunctionExpression(name, nullable, returnType, typeMapping); /// - public virtual SqlFunctionExpression NiladicFunction( + public virtual SqlExpression NiladicFunction( string schema, string name, bool nullable, Type returnType, RelationalTypeMapping? typeMapping = null) - => new(schema, name, nullable, returnType, typeMapping); + => new SqlFunctionExpression(schema, name, nullable, returnType, typeMapping); /// - public virtual SqlFunctionExpression NiladicFunction( + public virtual SqlExpression NiladicFunction( SqlExpression instance, string name, bool nullable, bool instancePropagatesNullability, Type returnType, RelationalTypeMapping? typeMapping = null) - => new( + => new SqlFunctionExpression( ApplyDefaultTypeMapping(instance), name, nullable, instancePropagatesNullability, returnType, typeMapping); /// - public virtual ExistsExpression Exists(SelectExpression subquery) - => new(subquery, _boolTypeMapping); + public virtual SqlExpression Exists(SelectExpression subquery) + => new ExistsExpression(subquery, _boolTypeMapping); /// - public virtual InExpression In(SqlExpression item, SelectExpression subquery) + public virtual SqlExpression In(SqlExpression item, SelectExpression subquery) => ApplyTypeMappingOnIn(new InExpression(item, subquery, _boolTypeMapping)); /// - public virtual InExpression In(SqlExpression item, IReadOnlyList values) + public virtual SqlExpression In(SqlExpression item, IReadOnlyList values) => ApplyTypeMappingOnIn(new InExpression(item, values, _boolTypeMapping)); /// - public virtual InExpression In(SqlExpression item, SqlParameterExpression valuesParameter) + public virtual SqlExpression In(SqlExpression item, SqlParameterExpression valuesParameter) => ApplyTypeMappingOnIn(new InExpression(item, valuesParameter, _boolTypeMapping)); /// - public virtual LikeExpression Like(SqlExpression match, SqlExpression pattern, SqlExpression? escapeChar = null) - => (LikeExpression)ApplyDefaultTypeMapping(new LikeExpression(match, pattern, escapeChar, null)); + public virtual SqlExpression Like(SqlExpression match, SqlExpression pattern, SqlExpression? escapeChar = null) + => ApplyDefaultTypeMapping(new LikeExpression(match, pattern, escapeChar, null)); /// - public virtual SqlFragmentExpression Fragment(string sql) - => new(sql); + public virtual SqlExpression Fragment(string sql) + => new SqlFragmentExpression(sql); /// - public virtual SqlConstantExpression Constant(object value, RelationalTypeMapping? typeMapping = null) - => new(value, typeMapping); + public virtual SqlExpression Constant(object value, RelationalTypeMapping? typeMapping = null) + => new SqlConstantExpression(value, typeMapping); /// - public virtual SqlConstantExpression Constant(object? value, Type type, RelationalTypeMapping? typeMapping = null) - => new(value, type, typeMapping); + public virtual SqlExpression Constant(object? value, Type type, RelationalTypeMapping? typeMapping = null) + => new SqlConstantExpression(value, type, typeMapping); /// public virtual bool TryCreateLeast( diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index c8fb53f61cf..fe23e7f94fe 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -801,7 +801,7 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt subquery.Offset, subquery.Limit); - var predicate = VisitSqlBinary( + var predicate = Visit( _sqlExpressionFactory.Equal(subqueryProjection, item), allowOptimizedExpansion: true, out _); subquery.ApplyPredicate(predicate); subquery.ClearOrdering(); @@ -908,7 +908,7 @@ protected virtual SqlExpression VisitIn(InExpression inExpression, bool allowOpt result, (expr, nullableValue) => _sqlExpressionFactory.OrElse( expr, - VisitSqlBinary(_sqlExpressionFactory.Equal(item, nullableValue), allowOptimizedExpansion, out _))); + Visit(_sqlExpressionFactory.Equal(item, nullableValue), allowOptimizedExpansion, out _))); InExpression ProcessInExpressionValues( InExpression inExpression, @@ -1873,8 +1873,13 @@ private SqlExpression RewriteNullSemantics( return sqlBinaryExpression.Update(left, right); } - private SqlExpression SimplifyLogicalSqlBinaryExpression(SqlBinaryExpression sqlBinaryExpression) + private SqlExpression SimplifyLogicalSqlBinaryExpression(SqlExpression expression) { + if (expression is not SqlBinaryExpression sqlBinaryExpression) + { + return expression; + } + if (sqlBinaryExpression is { Left: SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary, @@ -1930,13 +1935,14 @@ private SqlExpression SimplifyLogicalSqlBinaryExpression(SqlBinaryExpression sql /// /// Attempts to simplify a unary not operation on a non-nullable operand. /// - /// The expression to simplify. + /// The expression to simplify. /// The simplified expression, or the original expression if it cannot be simplified. - protected virtual SqlExpression OptimizeNonNullableNotExpression(SqlUnaryExpression sqlUnaryExpression) + protected virtual SqlExpression OptimizeNonNullableNotExpression(SqlExpression expression) { - if (sqlUnaryExpression.OperatorType != ExpressionType.Not) + if (expression is not SqlUnaryExpression sqlUnaryExpression + || sqlUnaryExpression.OperatorType != ExpressionType.Not) { - return sqlUnaryExpression; + return expression; } switch (sqlUnaryExpression.Operand) @@ -2207,8 +2213,13 @@ protected virtual TableExpressionBase UpdateParameterCollection( SqlParameterExpression newCollectionParameter) => throw new InvalidOperationException(); - private SqlExpression ProcessNullNotNull(SqlUnaryExpression sqlUnaryExpression, bool operandNullable) + private SqlExpression ProcessNullNotNull(SqlExpression sqlExpression, bool operandNullable) { + if (sqlExpression is not SqlUnaryExpression sqlUnaryExpression) + { + return sqlExpression; + } + if (!operandNullable) { // when we know that operand is non-nullable: diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs index 8b04e50989d..873ca89fa1d 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs @@ -241,14 +241,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp // CONCAT_WS filters out nulls, but string.Join treats them as empty strings; so coalesce (which is a no-op for non-nullable // arguments). - arguments[i + 1] = sqlArgument switch - { - ColumnExpression { IsNullable: false } => sqlArgument, - SqlConstantExpression constantExpression => constantExpression.Value is null - ? _sqlExpressionFactory.Constant(string.Empty) - : constantExpression, - _ => Dependencies.SqlExpressionFactory.Coalesce(sqlArgument, _sqlExpressionFactory.Constant(string.Empty)) - }; + arguments[i + 1] = Dependencies.SqlExpressionFactory.Coalesce(sqlArgument, _sqlExpressionFactory.Constant(string.Empty)); } // CONCAT_WS never returns null; a null delimiter is interpreted as an empty string, and null arguments are skipped diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs index b2abb4be5cf..01fcba5be4e 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerDateTimeMemberTranslator.cs @@ -112,7 +112,7 @@ public class SqlServerDateTimeMemberTranslator( _ => null }; - SqlFunctionExpression DatePart(string part) + SqlExpression DatePart(string part) => sqlExpressionFactory.Function( "DATEPART", arguments: [sqlExpressionFactory.Fragment(part), instance!], diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringAggregateMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringAggregateMethodTranslator.cs index c217cebd548..f2bb4a7d19d 100644 --- a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringAggregateMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerStringAggregateMethodTranslator.cs @@ -80,14 +80,10 @@ public SqlServerStringAggregateMethodTranslator( } } - // STRING_AGG filters out nulls, but string.Join treats them as empty strings; coalesce unless we know we're aggregating over - // a non-nullable column. - if (sqlExpression is not ColumnExpression { IsNullable: false }) - { - sqlExpression = _sqlExpressionFactory.Coalesce( - sqlExpression, - _sqlExpressionFactory.Constant(string.Empty, typeof(string))); - } + // STRING_AGG filters out nulls, but string.Join treats them as empty strings. + sqlExpression = _sqlExpressionFactory.Coalesce( + sqlExpression, + _sqlExpressionFactory.Constant(string.Empty, typeof(string))); // STRING_AGG returns null when there are no rows (or non-null values), but string.Join returns an empty string. return diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs index 14a7330f642..a80c1647f5a 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs @@ -35,7 +35,7 @@ public SqliteSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) /// 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 virtual SqlFunctionExpression Strftime( + public virtual SqlExpression Strftime( Type returnType, string format, SqlExpression timestring, @@ -82,7 +82,7 @@ public virtual SqlFunctionExpression Strftime( /// 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 virtual SqlFunctionExpression Date( + public virtual SqlExpression Date( Type returnType, SqlExpression timestring, IEnumerable? modifiers = null, diff --git a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringAggregateMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringAggregateMethodTranslator.cs index 621fc1ef222..b588eaa7fdf 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringAggregateMethodTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/Translators/SqliteStringAggregateMethodTranslator.cs @@ -60,12 +60,9 @@ public SqliteStringAggregateMethodTranslator(ISqlExpressionFactory sqlExpression return null; } - if (sqlExpression is not ColumnExpression { IsNullable: false }) - { - sqlExpression = _sqlExpressionFactory.Coalesce( - sqlExpression, - _sqlExpressionFactory.Constant(string.Empty, typeof(string))); - } + sqlExpression = _sqlExpressionFactory.Coalesce( + sqlExpression, + _sqlExpressionFactory.Constant(string.Empty, typeof(string))); if (source.Predicate != null) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index f948f7fc776..098b7169466 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -6378,7 +6378,7 @@ public override async Task GetValueOrDefault_in_filter_non_nullable_column(bool """ SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] -WHERE COALESCE([w].[Id], 0) = 0 +WHERE [w].[Id] = 0 """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 22e58c54fc5..bafb8cd54ca 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -8662,7 +8662,7 @@ public override async Task GetValueOrDefault_in_filter_non_nullable_column(bool """ SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] -WHERE COALESCE([w].[Id], 0) = 0 +WHERE [w].[Id] = 0 """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index cadd97ff667..d906429c4fe 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -7260,7 +7260,7 @@ public override async Task GetValueOrDefault_in_filter_non_nullable_column(bool """ SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] -WHERE COALESCE([w].[Id], 0) = 0 +WHERE [w].[Id] = 0 """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index efe49c9bce9..7374c5a47ee 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -7311,7 +7311,7 @@ public override async Task GetValueOrDefault_in_filter_non_nullable_column(bool """ SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[PeriodEnd], [w].[PeriodStart], [w].[SynergyWithId] FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] -WHERE COALESCE([w].[Id], 0) = 0 +WHERE [w].[Id] = 0 """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index a01b1bcc00d..d1a14d2e820 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -3096,7 +3096,7 @@ public override async Task GetValueOrDefault_in_filter_non_nullable_column(bool """ SELECT "w"."Id", "w"."AmmunitionType", "w"."IsAutomatic", "w"."Name", "w"."OwnerFullName", "w"."SynergyWithId" FROM "Weapons" AS "w" -WHERE COALESCE("w"."Id", 0) = 0 +WHERE "w"."Id" = 0 """); }