Skip to content

Commit

Permalink
Only use length-based sized type inference for string concat (#33403)
Browse files Browse the repository at this point in the history
* Only use length-based sized type inference for string concat

As opposed to when an `Add` note is for something other than concatenation.

Fixes #33330

* Updated based on review feedback
  • Loading branch information
ajcvickers authored Mar 27, 2024
1 parent bf52827 commit 15b129b
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 13 deletions.
6 changes: 5 additions & 1 deletion src/EFCore.Relational/Query/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
break;
}

case ExpressionType.Add:
case ExpressionType.Add when IsForString(left.TypeMapping) || IsForString(right.TypeMapping):
inferredTypeMapping = typeMapping;

if (inferredTypeMapping is null)
Expand Down Expand Up @@ -249,6 +249,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
resultTypeMapping = inferredTypeMapping;
break;

case ExpressionType.Add:
case ExpressionType.Subtract:
case ExpressionType.Multiply:
case ExpressionType.Divide:
Expand All @@ -274,6 +275,9 @@ private SqlExpression ApplyTypeMappingOnSqlBinary(
ApplyTypeMapping(right, inferredTypeMapping),
resultType,
resultTypeMapping);

static bool IsForString(RelationalTypeMapping? typeMapping)
=> (typeMapping?.Converter?.ProviderClrType ?? typeMapping?.ClrType) == typeof(string);
}

private InExpression ApplyTypeMappingOnIn(InExpression inExpression)
Expand Down
12 changes: 12 additions & 0 deletions test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8521,6 +8521,18 @@ await AssertQuery(
ss => ss.Set<Weapon>().Where(x => keys.Contains(ammoTypes.Contains(x.AmmunitionType) ? key : key)));
}


[ConditionalTheory]
[MemberData(nameof(IsAsyncData))] // Issue #33330
public virtual Task Non_string_concat_uses_appropriate_type_mapping(bool async)
{
var interval = TimeSpan.FromTicks(10);

return AssertQuery(
async,
ss => ss.Set<Mission>().Select(e => e.Duration + interval));
}

protected GearsOfWarContext CreateContext()
=> Fixture.CreateContext();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10250,7 +10250,7 @@ public override async Task Include_one_to_many_on_composite_key_then_orderby_key
await base.Include_one_to_many_on_composite_key_then_orderby_key_properties(async);

AssertSql(
"""
"""
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
FROM [Gears] AS [g]
LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName]
Expand Down Expand Up @@ -10279,7 +10279,7 @@ public override async Task Join_include_coalesce_simple(bool async)
await base.Join_include_coalesce_simple(async);

AssertSql(
"""
"""
SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId], CASE
WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
Expand All @@ -10289,16 +10289,16 @@ FROM [Gears] AS [g]
LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName]
ORDER BY [g].[Nickname], [g].[SquadId], [g0].[Nickname], [g0].[SquadId]
""",
//
"""
//
"""
SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [g].[Nickname], [g].[SquadId], [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Gears] AS [g]
LEFT JOIN [Gears] AS [g0] ON [g].[LeaderNickname] = [g0].[Nickname]
LEFT JOIN [Weapons] AS [w] ON [g0].[FullName] = [w].[OwnerFullName]
ORDER BY [g].[Nickname], [g].[SquadId], [g0].[Nickname], [g0].[SquadId]
""",
//
"""
//
"""
SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [g].[Nickname], [g].[SquadId], [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId]
FROM [Gears] AS [g]
LEFT JOIN [Gears] AS [g0] ON [g].[LeaderNickname] = [g0].[Nickname]
Expand All @@ -10313,7 +10313,7 @@ public override async Task Join_include_coalesce_nested(bool async)
await base.Join_include_coalesce_nested(async);

AssertSql(
"""
"""
SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId], CASE
WHEN [g].[Nickname] = N'Marcus' THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
Expand All @@ -10323,8 +10323,8 @@ FROM [Gears] AS [g]
LEFT JOIN [Weapons] AS [w] ON [g].[FullName] = [w].[OwnerFullName]
ORDER BY [g].[Nickname], [g].[SquadId], [g0].[Nickname], [g0].[SquadId]
""",
//
"""
//
"""
SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [g].[Nickname], [g].[SquadId], [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId], [w0].[Id], [w0].[AmmunitionType], [w0].[IsAutomatic], [w0].[Name], [w0].[OwnerFullName], [w0].[SynergyWithId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [w1].[Id], [w1].[AmmunitionType], [w1].[IsAutomatic], [w1].[Name], [w1].[OwnerFullName], [w1].[SynergyWithId]
FROM [Gears] AS [g]
LEFT JOIN [Gears] AS [g0] ON [g].[LeaderNickname] = [g0].[Nickname]
Expand All @@ -10340,7 +10340,7 @@ public override async Task Join_include_conditional(bool async)
await base.Join_include_conditional(async);

AssertSql(
"""
"""
SELECT CASE
WHEN [g0].[Nickname] IS NOT NULL AND [g0].[SquadId] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
Expand All @@ -10360,7 +10360,7 @@ public override async Task Derived_reference_is_skipped_when_base_type(bool asyn
await base.Derived_reference_is_skipped_when_base_type(async);

AssertSql(
"""
"""
SELECT [l].[Name], [l].[Discriminator], [l].[LocustHordeId], [l].[ThreatLevel], [l].[ThreatLevelByte], [l].[ThreatLevelNullableByte], [l].[DefeatedByNickname], [l].[DefeatedBySquadId], [l].[HighCommandId], [l0].[Id], [l0].[IsOperational], [l0].[Name]
FROM [LocustLeaders] AS [l]
LEFT JOIN [LocustHighCommands] AS [l0] ON [l].[HighCommandId] = [l0].[Id]
Expand Down Expand Up @@ -10411,6 +10411,18 @@ FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k]
""");
}

public override async Task Non_string_concat_uses_appropriate_type_mapping(bool async)
{
await base.Non_string_concat_uses_appropriate_type_mapping(async);

AssertSql(
"""
SELECT [m].[Duration]
FROM [Missions] AS [m]
"""
);
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ public TPCGearsOfWarQuerySqlServerTest(TPCGearsOfWarQuerySqlServerFixture fixtur
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());

public override async Task Non_string_concat_uses_appropriate_type_mapping(bool async)
{
await base.Non_string_concat_uses_appropriate_type_mapping(async);

AssertSql(
"""
SELECT [m].[Duration]
FROM [Missions] AS [m]
""");
}

public override async Task Entity_equality_empty(bool async)
{
await base.Entity_equality_empty(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ public TPTGearsOfWarQuerySqlServerTest(TPTGearsOfWarQuerySqlServerFixture fixtur
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());

public override async Task Non_string_concat_uses_appropriate_type_mapping(bool async)
{
await base.Non_string_concat_uses_appropriate_type_mapping(async);

AssertSql(
"""
SELECT [m].[Duration]
FROM [Missions] AS [m]
""");
}

public override async Task Entity_equality_empty(bool async)
{
await base.Entity_equality_empty(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ protected override Expression RewriteServerQueryExpression(Expression serverQuer
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());

public override async Task Non_string_concat_uses_appropriate_type_mapping(bool async)
{
await base.Non_string_concat_uses_appropriate_type_mapping(async);

AssertSql(
"""
SELECT [m].[Duration]
FROM [Missions] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [m]
""");
}

public override async Task Include_where_list_contains_navigation(bool async)
{
await Assert.ThrowsAsync<EqualException>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ public GearsOfWarQuerySqliteTest(GearsOfWarQuerySqliteFixture fixture, ITestOutp
public virtual void Check_all_tests_overridden()
=> TestHelpers.AssertAllMethodsOverridden(GetType());

public override async Task Non_string_concat_uses_appropriate_type_mapping(bool async)
{
await base.Non_string_concat_uses_appropriate_type_mapping(async);

AssertSql(
"""
SELECT "m"."Duration"
FROM "Missions" AS "m"
""");
}

public override async Task Where_datetimeoffset_date_component(bool async)
{
await AssertTranslationFailed(() => base.Where_datetimeoffset_date_component(async));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ WHERE [c].[CustomerID] <= N'ALFKI'")
AssertSql(
"SELECT [p].[ProductID], [p].[Discontinued], [p].[ProductName], [p].[SupplierID], [p].[UnitPrice], [p].[UnitsInStock]
FROM [Products] AS [p]
WHERE [p].[UnitsInStock] + 1 = 102")
WHERE [p].[UnitsInStock] + CAST(1 AS smallint) = CAST(102 AS smallint)")
End Sub

<ConditionalTheory>
Expand Down

0 comments on commit 15b129b

Please sign in to comment.