diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 1a408f27e20..c64e09a5c9f 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -202,6 +202,40 @@ private SqlExpression ApplyTypeMappingOnLike(LikeExpression likeExpression) } case ExpressionType.Add: + inferredTypeMapping = typeMapping; + + if (inferredTypeMapping is null) + { + // Infer null size (nvarchar(max)) if either side has no size. + // Note that for constants, we could instead look at the value length; but that requires we know the type mappings which + // can have a size (string/byte[], maybe something else?). + var inferredSize = left.TypeMapping?.Size is int leftSize && right.TypeMapping?.Size is int rightSize + ? leftSize + rightSize + : (int?)null; + + // Unless both sides are fixed length, the result isn't fixed length. + var inferredFixedLength = left.TypeMapping?.IsFixedLength is true && right.TypeMapping?.IsFixedLength is true; + // Default to Unicode unless both sides are non-unicode. + var inferredUnicode = !(left.TypeMapping?.IsUnicode is false && right.TypeMapping?.IsUnicode is false); + + var baseTypeMapping = left.TypeMapping + ?? right.TypeMapping + ?? ApplyDefaultTypeMapping(left).TypeMapping + ?? throw new InvalidOperationException("Couldn't find type mapping"); + + inferredTypeMapping = baseTypeMapping.Size == inferredSize + && baseTypeMapping.IsFixedLength == inferredFixedLength + && baseTypeMapping.IsUnicode == inferredUnicode + ? baseTypeMapping + : _typeMappingSource.FindMapping( + baseTypeMapping.ClrType, storeTypeName: null, keyOrIndex: false, inferredUnicode, inferredSize, + rowVersion: false, inferredFixedLength, baseTypeMapping.Precision, baseTypeMapping.Scale); + } + + resultType = inferredTypeMapping?.ClrType ?? left.Type; + resultTypeMapping = inferredTypeMapping; + break; + case ExpressionType.Subtract: case ExpressionType.Multiply: case ExpressionType.Divide: diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index a2e9be9df3e..d89e1af14c6 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -5659,4 +5659,26 @@ public virtual Task Subquery_with_navigation_inside_inline_collection(bool async => AssertQuery( async, ss => ss.Set().Where(c => new[] { 100, c.Orders.Count }.Sum() > 101)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_over_concatenated_columns_with_different_sizes(bool async) + { + var data = new[] { "ALFKI" + "Alfreds Futterkiste", "ANATR" + "Ana Trujillo Emparedados y helados" }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => data.Contains(c.CustomerID + c.CompanyName))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_over_concatenated_column_and_constant(bool async) + { + var data = new[] { "ALFKI" + "SomeConstant", "ANATR" + "SomeConstant" }; + + return AssertQuery( + async, + ss => ss.Set().Where(c => data.Contains(c.CustomerID + "SomeConstant"))); + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index 2b39872ad92..21f4e00bb04 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -7277,6 +7277,40 @@ SELECT COUNT(*) """); } + public override async Task Contains_over_concatenated_columns_with_different_sizes(bool async) + { + await base.Contains_over_concatenated_columns_with_different_sizes (async); + + AssertSql( + """ +@__data_0='["ALFKIAlfreds Futterkiste","ANATRAna Trujillo Emparedados y helados"]' (Size = 4000) + +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].[CustomerID] + [c].[CompanyName] IN ( + SELECT [d].[value] + FROM OPENJSON(@__data_0) WITH ([value] nvarchar(45) '$') AS [d] +) +"""); + } + + public override async Task Contains_over_concatenated_column_and_constant(bool async) + { + await base.Contains_over_concatenated_column_and_constant (async); + + AssertSql( + """ +@__data_0='["ALFKISomeConstant","ANATRSomeConstant"]' (Size = 4000) + +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].[CustomerID] + N'SomeConstant' IN ( + SELECT [d].[value] + FROM OPENJSON(@__data_0) WITH ([value] nvarchar(max) '$') AS [d] +) +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);