Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 49 additions & 27 deletions src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,40 +258,62 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
case { IsBuiltIn: true, Arguments: not null }
when string.Equals(sqlFunctionExpression.Name, "COALESCE", StringComparison.OrdinalIgnoreCase):
{
var type = sqlFunctionExpression.Type;
var typeMapping = sqlFunctionExpression.TypeMapping;
var defaultTypeMapping = _typeMappingSource.FindMapping(type);

// ISNULL always return a value having the same type as its first
// argument. Ideally we would convert the argument to have the
// desired type and type mapping, but currently EFCore has some
// trouble in computing types of non-homogeneous expressions
// (tracked in https://github.com/dotnet/efcore/issues/15586). To
// stay on the safe side we only use ISNULL if:
// - all sub-expressions have the same type as the expression
// - all sub-expressions have the same type mapping as the expression
// - the expression is using the default type mapping (combined
// with the two above, this implies that all of the expressions
// are using the default type mapping of the type)
if (defaultTypeMapping == typeMapping
&& sqlFunctionExpression.Arguments.All(a => a.Type == type && a.TypeMapping == typeMapping))
// (tracked in https://github.com/dotnet/efcore/issues/15586).
//
// The main issue is the sizing of the type. Since sometimes the
// computed size is wrong, stay on the safe side by expanding to the
// maximum supported size with an approach similar to that used in
// SqlServerStringAggregateMethodTranslator. This might result in
// unneeded conversions, but should produce the correct results.
var forceCast = sqlFunctionExpression.TypeMapping?.StoreTypeNameBase is
"nvarchar" or "varchar" or "varbinary";

var typeMapping = sqlFunctionExpression.TypeMapping switch
{
{ StoreTypeNameBase: "nvarchar", Size: >= 0 and < 4000 } => _typeMappingSource.FindMapping(
typeof(string),
sqlFunctionExpression.TypeMapping.StoreTypeNameBase,
unicode: true,
size: 4000),
{ StoreTypeNameBase: "varchar" or "varbinary", Size: >= 0 and < 8000 } => _typeMappingSource.FindMapping(
typeof(string),
sqlFunctionExpression.TypeMapping.StoreTypeNameBase,
unicode: false,
size: 8000),
var t => t,
};

var result = sqlFunctionExpression.Arguments[0];
if (forceCast || result.TypeMapping?.StoreType != typeMapping?.StoreType)
{
result = new SqlUnaryExpression(
ExpressionType.Convert,
result,
sqlFunctionExpression.Type,
typeMapping
);
}

var length = sqlFunctionExpression.Arguments.Count;
for (var i = 1; i < length; i++)
{
var head = sqlFunctionExpression.Arguments[0];
sqlFunctionExpression = (SqlFunctionExpression)sqlFunctionExpression
.Arguments
.Skip(1)
.Aggregate(
head, (l, r) => new SqlFunctionExpression(
"ISNULL",
arguments: [l, r],
nullable: true,
argumentsPropagateNullability: [false, false],
sqlFunctionExpression.Type,
sqlFunctionExpression.TypeMapping
));
// propagate type and type mapping from the first argument,
// nullability from COALESCE
result = new SqlFunctionExpression(
"ISNULL",
arguments: [result, sqlFunctionExpression.Arguments[i]],
nullable: i == length - 1 ? sqlFunctionExpression.IsNullable : true,
argumentsPropagateNullability: [false, false],
result.Type,
result.TypeMapping
);
}

return base.VisitSqlFunction(sqlFunctionExpression);
return base.VisitSqlFunction((SqlFunctionExpression)result);
}

case SqlServerJsonObjectExpression jsonObject:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3250,6 +3250,38 @@ public override Task Coalesce_Correct_TypeMapping_String(bool async)
SELECT VALUE ((c["Region"] != null) ? c["Region"] : "no region specified")
FROM root c
ORDER BY c["id"]
""");
});

public override Task Coalesce_Correct_TypeMapping_String_Sum(bool async)
=> Fixture.NoSyncTest(
async, async a =>
{
await base.Coalesce_Correct_TypeMapping_String_Sum(async);

AssertSql(
"""
SELECT VALUE ((((c["Region"] != null) ? ("R" || c["Region"]) : null) != null) ? ((c["Region"] != null) ? ("R" || c["Region"]) : null) : "no region specified")
FROM root c
ORDER BY c["id"]
""");
});

public override Task Coalesce_Correct_TypeMapping_String_Join(bool async)
=> Fixture.NoSyncTest(
async, async a =>
{
await base.Coalesce_Correct_TypeMapping_String_Join(async);

AssertSql(
"""
SELECT VALUE
{
"c" : (c["Region"] != null),
"c0" : ["R", c["Region"]]
}
FROM root c
ORDER BY c["id"]
""");
});

Expand Down Expand Up @@ -3367,7 +3399,7 @@ public override async Task SelectMany_primitive_select_subquery(bool async)
// Cosmos client evaluation. Issue #17246.
Assert.Equal(
CoreStrings.ExpressionParameterizationExceptionSensitive(
"value(Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryTestBase`1+<>c__DisplayClass177_0[Microsoft.EntityFrameworkCore.Query.NorthwindQueryCosmosFixture`1[Microsoft.EntityFrameworkCore.TestUtilities.NoopModelCustomizer]]).ss.Set().Any()"),
"value(Microsoft.EntityFrameworkCore.Query.NorthwindMiscellaneousQueryTestBase`1+<>c__DisplayClass179_0[Microsoft.EntityFrameworkCore.Query.NorthwindQueryCosmosFixture`1[Microsoft.EntityFrameworkCore.TestUtilities.NoopModelCustomizer]]).ss.Set().Any()"),
(await Assert.ThrowsAsync<InvalidOperationException>(() => base.SelectMany_primitive_select_subquery(async))).Message);

AssertSql();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,21 @@ public virtual Task Coalesce_Correct_TypeMapping_String(bool async)
ss => ss.Set<Customer>().OrderBy(c => c.CustomerID).Select(c => c.Region ?? "no region specified"),
assertOrder: true);

[ConditionalTheory, MemberData(nameof(IsAsyncData))]
public virtual Task Coalesce_Correct_TypeMapping_String_Sum(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>().OrderBy(c => c.CustomerID).Select(c => (c.Region != null ? "R" + c.Region : null) ?? "no region specified"),
assertOrder: true);

[ConditionalTheory, MemberData(nameof(IsAsyncData))]
public virtual Task Coalesce_Correct_TypeMapping_String_Join(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>().OrderBy(c => c.CustomerID)
.Select(c => (c.Region != null ? string.Join("|", new[] { "R", c.Region }) : null) ?? "no region specified"),
assertOrder: true);

[ConditionalTheory, MemberData(nameof(IsAsyncData))]
public virtual Task Null_Coalesce_Short_Circuit(bool async)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public override async Task Update_non_owned_property_on_entity_with_owned2(bool
AssertSql(
"""
UPDATE [o]
SET [o].[Title] = COALESCE([o].[Title], N'') + N'_Suffix'
SET [o].[Title] = ISNULL(CAST([o].[Title] AS nvarchar(max)), N'') + N'_Suffix'
FROM [Owner] AS [o]
""");
}
Expand All @@ -125,7 +125,7 @@ public override async Task Update_owned_and_non_owned_properties_with_table_shar
AssertSql(
"""
UPDATE [o]
SET [o].[Title] = COALESCE(CONVERT(varchar(11), [o].[OwnedReference_Number]), ''),
SET [o].[Title] = ISNULL(CAST(CONVERT(varchar(11), [o].[OwnedReference_Number]) AS varchar(8000)), ''),
[o].[OwnedReference_Number] = CAST(LEN([o].[Title]) AS int)
FROM [Owner] AS [o]
""");
Expand Down Expand Up @@ -190,7 +190,7 @@ public override async Task Update_with_alias_uniquification_in_setter_subquery(b
"""
UPDATE [o]
SET [o].[Total] = (
SELECT COALESCE(SUM([o0].[Amount]), 0)
SELECT ISNULL(SUM([o0].[Amount]), 0)
FROM [OrderProduct] AS [o0]
WHERE [o].[Id] = [o0].[OrderId])
FROM [Orders] AS [o]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ public override async Task Update_Where_set_property_plus_constant(bool async)
AssertExecuteUpdateSql(
"""
UPDATE [c]
SET [c].[ContactName] = COALESCE([c].[ContactName], N'') + N'Abc'
SET [c].[ContactName] = ISNULL(CAST([c].[ContactName] AS nvarchar(4000)), N'') + N'Abc'
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'F%'
""");
Expand All @@ -1139,7 +1139,7 @@ public override async Task Update_Where_set_property_plus_parameter(bool async)
@value='Abc' (Size = 4000)

UPDATE [c]
SET [c].[ContactName] = COALESCE([c].[ContactName], N'') + @value
SET [c].[ContactName] = ISNULL(CAST([c].[ContactName] AS nvarchar(4000)), N'') + @value
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'F%'
""");
Expand All @@ -1152,7 +1152,7 @@ public override async Task Update_Where_set_property_plus_property(bool async)
AssertExecuteUpdateSql(
"""
UPDATE [c]
SET [c].[ContactName] = COALESCE([c].[ContactName], N'') + [c].[CustomerID]
SET [c].[ContactName] = ISNULL(CAST([c].[ContactName] AS nvarchar(4000)), N'') + [c].[CustomerID]
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'F%'
""");
Expand Down Expand Up @@ -1574,11 +1574,11 @@ public override async Task Update_Where_Join_set_property_from_joined_single_res
AssertExecuteUpdateSql(
"""
UPDATE [c]
SET [c].[City] = COALESCE(CONVERT(varchar(11), DATEPART(year, (
SET [c].[City] = ISNULL(CAST(CONVERT(varchar(11), DATEPART(year, (
SELECT TOP(1) [o].[OrderDate]
FROM [Orders] AS [o]
WHERE [c].[CustomerID] = [o].[CustomerID]
ORDER BY [o].[OrderDate] DESC))), '')
ORDER BY [o].[OrderDate] DESC))) AS varchar(8000)), '')
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'F%'
""");
Expand Down Expand Up @@ -1609,11 +1609,11 @@ public override async Task Update_Where_Join_set_property_from_joined_single_res
AssertExecuteUpdateSql(
"""
UPDATE [c]
SET [c].[City] = COALESCE(CONVERT(varchar(11), DATEPART(year, (
SET [c].[City] = ISNULL(CAST(CONVERT(varchar(11), DATEPART(year, (
SELECT TOP(1) [o].[OrderDate]
FROM [Orders] AS [o]
WHERE [c].[CustomerID] = [o].[CustomerID]
ORDER BY [o].[OrderDate] DESC))), '')
ORDER BY [o].[OrderDate] DESC))) AS varchar(8000)), '')
FROM [Customers] AS [c]
WHERE [c].[CustomerID] LIKE N'F%'
""");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2356,7 +2356,7 @@ public override async Task Enum_with_value_converter_matching_take_value(bool as
@orderItemType='MyType1' (Nullable = false) (Size = 4000)
@p='1'

SELECT [o1].[Id], COALESCE((
SELECT [o1].[Id], ISNULL((
SELECT TOP(1) [o3].[Price]
FROM [OrderItems] AS [o3]
WHERE [o1].[Id] = [o3].[OrderId] AND [o3].[Type] = @orderItemType), 0.0E0) AS [SpecialSum]
Expand Down Expand Up @@ -2438,8 +2438,8 @@ public override async Task Group_by_aggregate_in_subquery_projection_after_group

AssertSql(
"""
SELECT [t].[Value] AS [A], COALESCE(SUM([t].[Id]), 0) AS [B], COALESCE((
SELECT TOP(1) COALESCE(SUM([t].[Id]), 0) + COALESCE(SUM([t0].[Id]), 0)
SELECT [t].[Value] AS [A], ISNULL(SUM([t].[Id]), 0) AS [B], ISNULL((
SELECT TOP(1) ISNULL(SUM([t].[Id]), 0) + ISNULL(SUM([t0].[Id]), 0)
FROM [Tables] AS [t0]
GROUP BY [t0].[Value]
ORDER BY (SELECT 1)), 0) AS [C]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,14 +337,14 @@ FROM [CompetitionSeasons] AS [c1]
SELECT [a].[Id], [a].[ActivityTypeId], [a].[DateTime], [a].[Points], (
SELECT TOP(1) [c].[Id]
FROM [CompetitionSeasons] AS [c]
WHERE [c].[StartDate] <= [a].[DateTime] AND [a].[DateTime] < [c].[EndDate]) AS [CompetitionSeasonId], COALESCE([a].[Points], (
WHERE [c].[StartDate] <= [a].[DateTime] AND [a].[DateTime] < [c].[EndDate]) AS [CompetitionSeasonId], ISNULL(ISNULL([a].[Points], (
SELECT TOP(1) [a1].[Points]
FROM [ActivityTypePoints] AS [a1]
INNER JOIN [CompetitionSeasons] AS [c0] ON [a1].[CompetitionSeasonId] = [c0].[Id]
WHERE [a0].[Id] = [a1].[ActivityTypeId] AND [c0].[Id] = (
SELECT TOP(1) [c1].[Id]
FROM [CompetitionSeasons] AS [c1]
WHERE [c1].[StartDate] <= [a].[DateTime] AND [a].[DateTime] < [c1].[EndDate])), 0) AS [Points]
WHERE [c1].[StartDate] <= [a].[DateTime] AND [a].[DateTime] < [c1].[EndDate]))), 0) AS [Points]
FROM [Activities] AS [a]
INNER JOIN [ActivityType] AS [a0] ON [a].[ActivityTypeId] = [a0].[Id]
""");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ public override async Task GroupBy()
SELECT [r].[Id], [r].[Name], [r].[AssociateCollection], [r].[OptionalAssociate], [r].[RequiredAssociate]
FROM [RootEntity] AS [r]
WHERE 16 IN (
SELECT COALESCE(SUM([a].[Int]), 0)
SELECT ISNULL(SUM([a].[Int]), 0)
FROM OPENJSON([r].[AssociateCollection], '$') WITH (
[Int] int '$.Int',
[String] nvarchar(max) '$.String'
Expand Down Expand Up @@ -341,7 +341,7 @@ FROM [RootEntity] AS [r]
AssertSql(
"""
SELECT (
SELECT COALESCE(SUM([s].[value]), 0)
SELECT ISNULL(SUM([s].[value]), 0)
FROM OPENJSON([r].[AssociateCollection], '$') WITH ([NestedCollection] nvarchar(max) '$.NestedCollection' AS JSON) AS [a]
OUTER APPLY (
SELECT MAX([n].[Int]) AS [value]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ public override async Task Select_Sum()
AssertSql(
"""
SELECT (
SELECT COALESCE(SUM([i0].[value]), 0)
SELECT ISNULL(SUM([i0].[value]), 0)
FROM OPENJSON(JSON_QUERY([r].[RequiredAssociate], '$.Ints')) WITH ([value] int '$') AS [i0])
FROM [RootEntity] AS [r]
WHERE (
SELECT COALESCE(SUM([i].[value]), 0)
SELECT ISNULL(SUM([i].[value]), 0)
FROM OPENJSON(JSON_QUERY([r].[RequiredAssociate], '$.Ints')) WITH ([value] int '$') AS [i]) >= 6
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ FROM [RootEntity] AS [r]
AssertSql(
"""
SELECT (
SELECT COALESCE(SUM([s].[value]), 0)
SELECT ISNULL(SUM([s].[value]), 0)
FROM (
SELECT [a].[NestedCollection] AS [NestedCollection]
FROM OPENJSON([r].[AssociateCollection], '$') WITH (
Expand All @@ -89,7 +89,7 @@ [NestedCollection] nvarchar(max) '$.NestedCollection' AS JSON
WHERE [a0].[String] = N'foo'
) AS [u]
OUTER APPLY (
SELECT COALESCE(SUM([n].[Int]), 0) AS [value]
SELECT ISNULL(SUM([n].[Int]), 0) AS [value]
FROM OPENJSON([u].[NestedCollection], '$') WITH ([Int] int '$.Int') AS [n]
) AS [s])
FROM [RootEntity] AS [r]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ public override async Task Select_Sum()
AssertSql(
"""
SELECT (
SELECT COALESCE(SUM([r1].[value]), 0)
SELECT ISNULL(SUM([r1].[value]), 0)
FROM OPENJSON([r].[RequiredAssociate_Ints]) WITH ([value] int '$') AS [r1])
FROM [RootEntity] AS [r]
WHERE (
SELECT COALESCE(SUM([r0].[value]), 0)
SELECT ISNULL(SUM([r0].[value]), 0)
FROM OPENJSON([r].[RequiredAssociate_Ints]) WITH ([value] int '$') AS [r0]) >= 6
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ FROM [AssociateType] AS [a2]
LEFT JOIN [NestedAssociateType] AS [n6] ON [a0].[Id] = [n6].[CollectionAssociateId]
LEFT JOIN [NestedAssociateType] AS [n7] ON [a1].[Id] = [n7].[CollectionAssociateId]
WHERE 16 IN (
SELECT COALESCE(SUM([a].[Int]), 0)
SELECT ISNULL(SUM([a].[Int]), 0)
FROM [AssociateType] AS [a]
WHERE [r].[Id] = [a].[CollectionRootId]
GROUP BY [a].[String]
Expand All @@ -260,7 +260,7 @@ public override async Task Select_within_Select_within_Select_with_aggregates()
AssertSql(
"""
SELECT (
SELECT COALESCE(SUM([s].[value]), 0)
SELECT ISNULL(SUM([s].[value]), 0)
FROM [AssociateType] AS [a]
OUTER APPLY (
SELECT MAX([n].[Int]) AS [value]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,12 @@ public override async Task Select_Sum()
AssertSql(
"""
SELECT (
SELECT COALESCE(SUM([i0].[value]), 0)
SELECT ISNULL(SUM([i0].[value]), 0)
FROM OPENJSON([a].[Ints]) WITH ([value] int '$') AS [i0])
FROM [RootEntity] AS [r]
INNER JOIN [AssociateType] AS [a] ON [r].[RequiredAssociateId] = [a].[Id]
WHERE (
SELECT COALESCE(SUM([i].[value]), 0)
SELECT ISNULL(SUM([i].[value]), 0)
FROM OPENJSON([a].[Ints]) WITH ([value] int '$') AS [i]) >= 6
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public override async Task Over_assocate_collection_Select_nested_with_aggregate
AssertSql(
"""
SELECT (
SELECT COALESCE(SUM([s].[value]), 0)
SELECT ISNULL(SUM([s].[value]), 0)
FROM (
SELECT [a].[Id]
FROM [AssociateType] AS [a]
Expand All @@ -71,7 +71,7 @@ FROM [AssociateType] AS [a0]
WHERE [r].[Id] = [a0].[CollectionRootId] AND [a0].[String] = N'foo'
) AS [u]
OUTER APPLY (
SELECT COALESCE(SUM([n].[Int]), 0) AS [value]
SELECT ISNULL(SUM([n].[Int]), 0) AS [value]
FROM [NestedAssociateType] AS [n]
WHERE [u].[Id] = [n].[CollectionAssociateId]
) AS [s])
Expand Down
Loading
Loading