@@ -213,37 +213,62 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
213
213
if ( sqlFunctionExpression is { IsBuiltIn : true , Arguments : not null }
214
214
&& string . Equals ( sqlFunctionExpression . Name , "COALESCE" , StringComparison . OrdinalIgnoreCase ) )
215
215
{
216
- var type = sqlFunctionExpression . Type ;
217
- var typeMapping = sqlFunctionExpression . TypeMapping ;
218
- var defaultTypeMapping = _typeMappingSource . FindMapping ( type ) ;
219
-
220
216
// ISNULL always return a value having the same type as its first
221
217
// argument. Ideally we would convert the argument to have the
222
218
// desired type and type mapping, but currently EFCore has some
223
219
// trouble in computing types of non-homogeneous expressions
224
- // (tracked in https://github.com/dotnet/efcore/issues/15586). To
225
- // stay on the safe side we only use ISNULL if:
226
- // - all sub-expressions have the same type as the expression
227
- // - all sub-expressions have the same type mapping as the expression
228
- // - the expression is using the default type mapping (combined
229
- // with the two above, this implies that all of the expressions
230
- // are using the default type mapping of the type)
231
- if ( defaultTypeMapping == typeMapping
232
- && sqlFunctionExpression . Arguments . All ( a => a . Type == type && a . TypeMapping == typeMapping ) ) {
233
-
234
- var head = sqlFunctionExpression . Arguments [ 0 ] ;
235
- sqlFunctionExpression = ( SqlFunctionExpression ) sqlFunctionExpression
236
- . Arguments
237
- . Skip ( 1 )
238
- . Aggregate ( head , ( l , r ) => new SqlFunctionExpression (
239
- "ISNULL" ,
240
- arguments : [ l , r ] ,
241
- nullable : true ,
242
- argumentsPropagateNullability : [ false , false ] ,
243
- sqlFunctionExpression . Type ,
244
- sqlFunctionExpression . TypeMapping
245
- ) ) ;
220
+ // (tracked in https://github.com/dotnet/efcore/issues/15586).
221
+ //
222
+ // The main issue is the sizing of the type. Since sometimes the
223
+ // computed size is wrong, stay on the safe side by expanding to the
224
+ // maximum supported size with an approach similar to that used in
225
+ // SqlServerStringAggregateMethodTranslator. This might result in
226
+ // unneeded conversions, but should produce the correct results.
227
+ var forceCast = sqlFunctionExpression . TypeMapping ? . StoreTypeNameBase is
228
+ "nvarchar" or "varchar" or "varbinary" ;
229
+
230
+ var typeMapping = sqlFunctionExpression . TypeMapping switch
231
+ {
232
+ { StoreTypeNameBase : "nvarchar" , Size : >= 0 and < 4000 } => _typeMappingSource . FindMapping (
233
+ typeof ( string ) ,
234
+ sqlFunctionExpression . TypeMapping . StoreTypeNameBase ,
235
+ unicode : true ,
236
+ size : 4000 ) ,
237
+ { StoreTypeNameBase : "varchar" or "varbinary" , Size : >= 0 and < 8000 } => _typeMappingSource . FindMapping (
238
+ typeof ( string ) ,
239
+ sqlFunctionExpression . TypeMapping . StoreTypeNameBase ,
240
+ unicode : false ,
241
+ size : 8000 ) ,
242
+ var t => t ,
243
+ } ;
244
+
245
+ var result = sqlFunctionExpression . Arguments [ 0 ] ;
246
+ if ( forceCast || result . TypeMapping ? . StoreType != typeMapping ? . StoreType )
247
+ {
248
+ result = new SqlUnaryExpression (
249
+ ExpressionType . Convert ,
250
+ result ,
251
+ sqlFunctionExpression . Type ,
252
+ typeMapping
253
+ ) ;
246
254
}
255
+
256
+ var length = sqlFunctionExpression . Arguments . Count ;
257
+ for ( var i = 1 ; i < length ; i ++ )
258
+ {
259
+ // propagate type and type mapping from the first argument,
260
+ // nullability from COALESCE
261
+ result = new SqlFunctionExpression (
262
+ "ISNULL" ,
263
+ arguments : [ result , sqlFunctionExpression . Arguments [ i ] ] ,
264
+ nullable : i == length - 1 ? sqlFunctionExpression . IsNullable : true ,
265
+ argumentsPropagateNullability : [ false , false ] ,
266
+ result . Type ,
267
+ result . TypeMapping
268
+ ) ;
269
+ }
270
+
271
+ sqlFunctionExpression = ( SqlFunctionExpression ) result ;
247
272
}
248
273
249
274
return base . VisitSqlFunction ( sqlFunctionExpression ) ;
0 commit comments