@@ -258,40 +258,62 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
258258 case { IsBuiltIn : true , Arguments : not null }
259259 when string . Equals ( sqlFunctionExpression . Name , "COALESCE" , StringComparison . OrdinalIgnoreCase ) :
260260 {
261- var type = sqlFunctionExpression . Type ;
262- var typeMapping = sqlFunctionExpression . TypeMapping ;
263- var defaultTypeMapping = _typeMappingSource . FindMapping ( type ) ;
264-
265261 // ISNULL always return a value having the same type as its first
266262 // argument. Ideally we would convert the argument to have the
267263 // desired type and type mapping, but currently EFCore has some
268264 // trouble in computing types of non-homogeneous expressions
269- // (tracked in https://github.com/dotnet/efcore/issues/15586). To
270- // stay on the safe side we only use ISNULL if:
271- // - all sub-expressions have the same type as the expression
272- // - all sub-expressions have the same type mapping as the expression
273- // - the expression is using the default type mapping (combined
274- // with the two above, this implies that all of the expressions
275- // are using the default type mapping of the type)
276- if ( defaultTypeMapping == typeMapping
277- && sqlFunctionExpression . Arguments . All ( a => a . Type == type && a . TypeMapping == typeMapping ) )
265+ // (tracked in https://github.com/dotnet/efcore/issues/15586).
266+ //
267+ // The main issue is the sizing of the type. Since sometimes the
268+ // computed size is wrong, stay on the safe side by expanding to the
269+ // maximum supported size with an approach similar to that used in
270+ // SqlServerStringAggregateMethodTranslator. This might result in
271+ // unneeded conversions, but should produce the correct results.
272+ var forceCast = sqlFunctionExpression . TypeMapping ? . StoreTypeNameBase is
273+ "nvarchar" or "varchar" or "varbinary" ;
274+
275+ var typeMapping = sqlFunctionExpression . TypeMapping switch
276+ {
277+ { StoreTypeNameBase : "nvarchar" , Size : >= 0 and < 4000 } => _typeMappingSource . FindMapping (
278+ typeof ( string ) ,
279+ sqlFunctionExpression . TypeMapping . StoreTypeNameBase ,
280+ unicode : true ,
281+ size : 4000 ) ,
282+ { StoreTypeNameBase : "varchar" or "varbinary" , Size : >= 0 and < 8000 } => _typeMappingSource . FindMapping (
283+ typeof ( string ) ,
284+ sqlFunctionExpression . TypeMapping . StoreTypeNameBase ,
285+ unicode : false ,
286+ size : 8000 ) ,
287+ var t => t ,
288+ } ;
289+
290+ var result = sqlFunctionExpression . Arguments [ 0 ] ;
291+ if ( forceCast || result . TypeMapping ? . StoreType != typeMapping ? . StoreType )
292+ {
293+ result = new SqlUnaryExpression (
294+ ExpressionType . Convert ,
295+ result ,
296+ sqlFunctionExpression . Type ,
297+ typeMapping
298+ ) ;
299+ }
300+
301+ var length = sqlFunctionExpression . Arguments . Count ;
302+ for ( var i = 1 ; i < length ; i ++ )
278303 {
279- var head = sqlFunctionExpression . Arguments [ 0 ] ;
280- sqlFunctionExpression = ( SqlFunctionExpression ) sqlFunctionExpression
281- . Arguments
282- . Skip ( 1 )
283- . Aggregate (
284- head , ( l , r ) => new SqlFunctionExpression (
285- "ISNULL" ,
286- arguments : [ l , r ] ,
287- nullable : true ,
288- argumentsPropagateNullability : [ false , false ] ,
289- sqlFunctionExpression . Type ,
290- sqlFunctionExpression . TypeMapping
291- ) ) ;
304+ // propagate type and type mapping from the first argument,
305+ // nullability from COALESCE
306+ result = new SqlFunctionExpression (
307+ "ISNULL" ,
308+ arguments : [ result , sqlFunctionExpression . Arguments [ i ] ] ,
309+ nullable : i == length - 1 ? sqlFunctionExpression . IsNullable : true ,
310+ argumentsPropagateNullability : [ false , false ] ,
311+ result . Type ,
312+ result . TypeMapping
313+ ) ;
292314 }
293315
294- return base . VisitSqlFunction ( sqlFunctionExpression ) ;
316+ return base . VisitSqlFunction ( ( SqlFunctionExpression ) result ) ;
295317 }
296318
297319 case SqlServerJsonObjectExpression jsonObject :
0 commit comments