diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index 073a15175f1..61f8913a44e 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -106,6 +106,7 @@ private SqlExpression ApplyTypeMappingOnSqlUnary( SqlUnaryExpression sqlUnaryExpression, CoreTypeMapping typeMapping) { SqlExpression operand; + Type resultType; CoreTypeMapping resultTypeMapping; switch (sqlUnaryExpression.OperatorType) { @@ -115,18 +116,23 @@ private SqlExpression ApplyTypeMappingOnSqlUnary( when sqlUnaryExpression.IsLogicalNot(): { resultTypeMapping = _boolTypeMapping; + resultType = typeof(bool); operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; } case ExpressionType.Convert: resultTypeMapping = typeMapping; + // Since we are applying convert, resultTypeMapping decides the clrType + resultType = resultTypeMapping?.ClrType ?? sqlUnaryExpression.Type; operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; case ExpressionType.Not: case ExpressionType.Negate: resultTypeMapping = typeMapping; + // While Not is logical, negate is numeric hence we use clrType from TypeMapping + resultType = resultTypeMapping?.ClrType ?? sqlUnaryExpression.Type; operand = ApplyTypeMapping(sqlUnaryExpression.Operand, typeMapping); break; @@ -134,11 +140,7 @@ when sqlUnaryExpression.IsLogicalNot(): throw new InvalidOperationException(CoreStrings.TranslationFailed(sqlUnaryExpression.Print())); } - return new SqlUnaryExpression( - sqlUnaryExpression.OperatorType, - operand, - sqlUnaryExpression.Type, - resultTypeMapping); + return new SqlUnaryExpression(sqlUnaryExpression.OperatorType, operand, resultType, resultTypeMapping); } private SqlExpression ApplyTypeMappingOnSqlBinary( @@ -160,11 +162,14 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( case ExpressionType.NotEqual: { inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right) - ?? _typeMappingSource.FindMapping(left.Type); + // We avoid object here since the result does not get typeMapping from outside. + ?? (left.Type != typeof(object) + ? _typeMappingSource.FindMapping(left.Type) + : _typeMappingSource.FindMapping(right.Type)); resultType = typeof(bool); resultTypeMapping = _boolTypeMapping; } - break; + break; case ExpressionType.AndAlso: case ExpressionType.OrElse: @@ -173,7 +178,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( resultType = typeof(bool); resultTypeMapping = _boolTypeMapping; } - break; + break; case ExpressionType.Add: case ExpressionType.Subtract: @@ -186,10 +191,10 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( case ExpressionType.Or: { inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right); - resultType = left.Type; + resultType = inferredTypeMapping?.ClrType ?? left.Type; resultTypeMapping = inferredTypeMapping; } - break; + break; default: throw new InvalidOperationException(CoreStrings.IncorrectOperatorType); diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index f247784b154..b064464fb19 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -106,6 +106,7 @@ private SqlExpression ApplyTypeMappingOnSqlUnary( SqlUnaryExpression sqlUnaryExpression, RelationalTypeMapping typeMapping) { SqlExpression operand; + Type resultType; RelationalTypeMapping resultTypeMapping; switch (sqlUnaryExpression.OperatorType) { @@ -115,18 +116,23 @@ private SqlExpression ApplyTypeMappingOnSqlUnary( when sqlUnaryExpression.IsLogicalNot(): { resultTypeMapping = _boolTypeMapping; + resultType = typeof(bool); operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; } case ExpressionType.Convert: resultTypeMapping = typeMapping; + // Since we are applying convert, resultTypeMapping decides the clrType + resultType = resultTypeMapping?.ClrType ?? sqlUnaryExpression.Type; operand = ApplyDefaultTypeMapping(sqlUnaryExpression.Operand); break; case ExpressionType.Not: case ExpressionType.Negate: resultTypeMapping = typeMapping; + // While Not is logical, negate is numeric hence we use clrType from TypeMapping + resultType = resultTypeMapping?.ClrType ?? sqlUnaryExpression.Type; operand = ApplyTypeMapping(sqlUnaryExpression.Operand, typeMapping); break; @@ -134,11 +140,7 @@ when sqlUnaryExpression.IsLogicalNot(): throw new InvalidOperationException(CoreStrings.UnsupportedUnary); } - return new SqlUnaryExpression( - sqlUnaryExpression.OperatorType, - operand, - sqlUnaryExpression.Type, - resultTypeMapping); + return new SqlUnaryExpression(sqlUnaryExpression.OperatorType, operand, resultType, resultTypeMapping); } private SqlExpression ApplyTypeMappingOnSqlBinary( @@ -160,7 +162,10 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( case ExpressionType.NotEqual: { inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right) - ?? _typeMappingSource.FindMapping(left.Type); + // We avoid object here since the result does not get typeMapping from outside. + ?? (left.Type != typeof(object) + ? _typeMappingSource.FindMapping(left.Type) + : _typeMappingSource.FindMapping(right.Type)); resultType = typeof(bool); resultTypeMapping = _boolTypeMapping; break; @@ -184,7 +189,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( case ExpressionType.Or: { inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right); - resultType = left.Type; + resultType = inferredTypeMapping?.ClrType ?? left.Type; resultTypeMapping = inferredTypeMapping; break; } @@ -437,7 +442,10 @@ public virtual CaseExpression Case( var operandTypeMapping = operand.TypeMapping ?? whenClauses.Select(wc => wc.Test.TypeMapping).FirstOrDefault(t => t != null) - ?? _typeMappingSource.FindMapping(operand.Type); + // Since we never look at type of Operand/Test after this place, + // we need to find actual typeMapping based on non-object type. + ?? new[] { operand.Type }.Concat(whenClauses.Select(wc => wc.Test.Type)) + .Where(t => t != typeof(object)).Select(t => _typeMappingSource.FindMapping(t)).FirstOrDefault(); var resultTypeMapping = elseResult?.TypeMapping ?? whenClauses.Select(wc => wc.Result.TypeMapping).FirstOrDefault(t => t != null); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index a73af113a20..e327a278972 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -2778,7 +2778,7 @@ public override async Task Concat_string_int(bool async) await base.Concat_string_int(async); AssertSql( - @"SELECT CAST([o].[OrderID] AS nchar(5)) + [o].[CustomerID] + @"SELECT CAST([o].[OrderID] AS nchar(5)) + COALESCE([o].[CustomerID], N'') FROM [Orders] AS [o]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index b2b83825c55..10f220cc74d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -1341,7 +1341,7 @@ public override async Task Where_concat_string_int_comparison4(bool async) AssertSql( @"SELECT [o].[CustomerID] FROM [Orders] AS [o] -WHERE ((CAST([o].[OrderID] AS nchar(5)) + [o].[CustomerID]) = [o].[CustomerID]) OR [o].[CustomerID] IS NULL"); +WHERE (CAST([o].[OrderID] AS nchar(5)) + COALESCE([o].[CustomerID], N'')) = [o].[CustomerID]"); } public override async Task Where_concat_string_string_comparison(bool async) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs index 321c9f0d37d..b20dd511cc2 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs @@ -242,10 +242,13 @@ public override Task Complex_nested_query_doesnt_try_binding_to_grandparent_when public override Task AsQueryable_in_query_server_evals(bool async) => null; - [ConditionalTheory(Skip = "Issue #19990")] public override async Task Concat_string_int(bool async) { await base.Concat_string_int(async); + + AssertSql( + @"SELECT CAST(""o"".""OrderID"" AS TEXT) || COALESCE(""o"".""CustomerID"", '') +FROM ""Orders"" AS ""o"""); } public override async Task Concat_int_string(bool async)