Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private SqlFunctionExpression(
Type type,
RelationalTypeMapping? typeMapping)
: this(
instance, schema, name, niladic: true, arguments: null, nullable, instancePropagatesNullability,
instance, schema, name, arguments: null, nullable, instancePropagatesNullability,
argumentsPropagateNullability: null, builtIn, type, typeMapping)
{
}
Expand Down Expand Up @@ -165,24 +165,6 @@ public SqlFunctionExpression(
{
}

private SqlFunctionExpression(
SqlExpression? instance,
string? schema,
string name,
IEnumerable<SqlExpression> arguments,
bool nullable,
bool? instancePropagatesNullability,
IEnumerable<bool> argumentsPropagateNullability,
bool builtIn,
Type type,
RelationalTypeMapping? typeMapping)
: this(
instance, schema, name, niladic: false, arguments, nullable,
instancePropagatesNullability, argumentsPropagateNullability, builtIn,
type, typeMapping)
{
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -194,7 +176,6 @@ public SqlFunctionExpression(
SqlExpression? instance,
string? schema,
string name,
bool niladic,
IEnumerable<SqlExpression>? arguments,
bool nullable,
bool? instancePropagatesNullability,
Expand All @@ -207,7 +188,6 @@ public SqlFunctionExpression(
Instance = instance;
Name = name;
Schema = schema;
IsNiladic = niladic;
IsBuiltIn = builtIn;
Arguments = arguments?.ToList();
IsNullable = nullable;
Expand Down Expand Up @@ -240,7 +220,7 @@ public SqlFunctionExpression(
/// A bool value indicating if the function is niladic.
/// </summary>
[MemberNotNullWhen(false, nameof(Arguments), nameof(ArgumentsPropagateNullability))]
public virtual bool IsNiladic { get; }
public virtual bool IsNiladic => Arguments is null;

/// <summary>
/// A bool value indicating if the function is built-in.
Expand Down Expand Up @@ -295,7 +275,6 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
instance,
Schema,
Name,
IsNiladic,
arguments,
IsNullable,
InstancePropagatesNullability,
Expand All @@ -316,7 +295,6 @@ public virtual SqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typ
Instance,
Schema,
Name,
IsNiladic,
Arguments,
IsNullable,
InstancePropagatesNullability,
Expand All @@ -333,33 +311,51 @@ public virtual SqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typ
/// <param name="arguments">The <see cref="Arguments" /> property of the result.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public virtual SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyList<SqlExpression>? arguments)
=> instance != Instance || (arguments != null && Arguments != null && !arguments.SequenceEqual(Arguments))
? new SqlFunctionExpression(
=> Update(instance, arguments, ArgumentsPropagateNullability);

/// <summary>
/// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
/// return this expression.
/// </summary>
/// <param name="instance">The <see cref="Instance" /> property of the result.</param>
/// <param name="arguments">The <see cref="Arguments" /> property of the result.</param>
/// <param name="argumentsPropagateNullability">The <see cref="ArgumentsPropagateNullability" /> property of the result. If omitted,
/// the current ArgumentsPropagateNullability will be used.</param>
/// <returns>This expression if no children changed, or an expression with the updated children.</returns>
public virtual SqlFunctionExpression Update(
SqlExpression? instance,
IReadOnlyList<SqlExpression>? arguments,
IReadOnlyList<bool>? argumentsPropagateNullability)
=> instance == Instance
&& ((arguments == Arguments) || (arguments != null && Arguments != null && arguments.SequenceEqual(Arguments)))
&& ((argumentsPropagateNullability == ArgumentsPropagateNullability)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, the problem this creates is that argumentsPropagateNullability being null could mean two things: either the user is switching to niladic (and so arguments is null too), or they want to just change the arguments without changing argumentsPropagateNullability (which seems like a common scenario).

Maybe we should just have two overloads, one only for changing arguments (doesn't have argumentsPropagateNullability parameter, and importantly, cannot change the number of arguments), and another with arguments and argumentsPropagateNullability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that (it was indeed my initial approach).
Note that the "common" scenario occurs just 3 times in the codebase (there are only 4 uses of this method)

|| (argumentsPropagateNullability != null
&& ArgumentsPropagateNullability != null
&& argumentsPropagateNullability.SequenceEqual(ArgumentsPropagateNullability)))
? this
: new SqlFunctionExpression(
instance,
Schema,
Name,
IsNiladic,
arguments,
IsNullable,
InstancePropagatesNullability,
ArgumentsPropagateNullability,
argumentsPropagateNullability,
IsBuiltIn,
Type,
TypeMapping)
: this;
TypeMapping);

/// <inheritdoc />
public override Expression Quote()
=> New(
_quotingConstructor ??= typeof(SqlFunctionExpression).GetConstructor(
[
typeof(SqlExpression), typeof(string), typeof(string), typeof(bool), typeof(IEnumerable<SqlExpression>),
typeof(SqlExpression), typeof(string), typeof(string), typeof(IEnumerable<SqlExpression>),
typeof(bool), typeof(bool), typeof(IEnumerable<bool>), typeof(bool), typeof(Type), typeof(RelationalTypeMapping)
])!,
RelationalExpressionQuotingUtilities.QuoteOrNull(Instance),
Constant(Schema, typeof(string)),
Constant(Name),
Constant(IsNiladic),
Arguments is null
? Constant(null, typeof(IEnumerable<SqlExpression>))
: NewArrayInit(typeof(SqlExpression), initializers: Arguments.Select(a => a.Quote())),
Expand Down Expand Up @@ -408,7 +404,6 @@ public override bool Equals(object? obj)

private bool Equals(SqlFunctionExpression sqlFunctionExpression)
=> base.Equals(sqlFunctionExpression)
&& IsNiladic == sqlFunctionExpression.IsNiladic
&& Name == sqlFunctionExpression.Name
&& Schema == sqlFunctionExpression.Schema
&& ((Instance == null && sqlFunctionExpression.Instance == null)
Expand Down
14 changes: 12 additions & 2 deletions src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1422,12 +1422,22 @@ protected virtual SqlExpression VisitSqlFunction(
foreach (var argument in sqlFunctionExpression.Arguments)
{
coalesceArguments.Add(Visit(argument, out var argumentNullable));
coalesceNullable = coalesceNullable && argumentNullable;
if (!argumentNullable)
{
coalesceNullable = false;
break;
}
}

nullable = coalesceNullable;

return sqlFunctionExpression.Update(sqlFunctionExpression.Instance, coalesceArguments);
return coalesceArguments.Count == 1
? coalesceArguments[0]
: sqlFunctionExpression.Update(
sqlFunctionExpression.Instance,
coalesceArguments,
argumentsPropagateNullability: coalesceArguments.Select(_ => false).ToArray()
);
}

var useNullabilityPropagation = sqlFunctionExpression is { InstancePropagatesNullability: true };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public SqlServerStringAggregateMethodTranslator(
source,
enumerableArgumentIndex: 0,
nullable: true,
argumentsPropagateNullability: new[] { false, true },
argumentsPropagateNullability: new[] { false, false },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch.

typeof(string)),
_sqlExpressionFactory.Constant(string.Empty, typeof(string)),
resultTypeMapping);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public SqliteStringAggregateMethodTranslator(ISqlExpressionFactory sqlExpression
sqlExpression.TypeMapping)
},
nullable: true,
argumentsPropagateNullability: new[] { false, true },
argumentsPropagateNullability: new[] { false, false },
typeof(string)),
_sqlExpressionFactory.Constant(string.Empty, typeof(string)),
sqlExpression.TypeMapping);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,18 @@ public virtual Task Where_multiple_ands_with_nullable_parameter_and_constant_not
public virtual Task Where_coalesce(bool async)
=> AssertQueryScalar(async, ss => ss.Set<NullSemanticsEntity1>().Where(e => e.NullableBoolA ?? true).Select(e => e.Id));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_coalesce_shortcircuit(bool async)
=> AssertQueryScalar(async, ss => ss.Set<NullSemanticsEntity1>().Where(e => (bool?)(e.BoolA | e.BoolB) ?? e.NullableBoolA ?? true)
.Select(e => e.Id));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_coalesce_shortcircuit_many(bool async)
=> AssertQueryScalar(async, ss => ss.Set<NullSemanticsEntity1>().Where(e => e.NullableBoolA ?? (bool?)(e.BoolA | e.BoolB) ?? e.NullableBoolB ?? e.BoolB)
.Select(e => e.Id));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Where_equal_nullable_with_null_value_parameter(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1215,10 +1215,10 @@ public override async Task Select_null_propagation_negative9(bool async)
AssertSql(
"""
SELECT CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN COALESCE(CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN CASE
WHEN CAST(LEN([g].[Nickname]) AS int) = 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, CAST(0 AS bit))
END
ELSE NULL
END
FROM [Gears] AS [g]
Expand Down Expand Up @@ -4033,7 +4033,7 @@ public override async Task ToString_enum_property_projection(bool async)
await base.ToString_enum_property_projection(async);

AssertSql(
"""
"""
SELECT CASE [g].[Rank]
WHEN 0 THEN N'None'
WHEN 1 THEN N'Private'
Expand All @@ -4044,7 +4044,7 @@ WHEN 16 THEN N'Captain'
WHEN 32 THEN N'Major'
WHEN 64 THEN N'Colonel'
WHEN 128 THEN N'General'
ELSE COALESCE(CAST([g].[Rank] AS nvarchar(max)), N'')
ELSE CAST([g].[Rank] AS nvarchar(max))
END
FROM [Gears] AS [g]
""");
Expand Down Expand Up @@ -5759,7 +5759,7 @@ SELECT COALESCE((
SELECT TOP(1) [w].[Id]
FROM [Weapons] AS [w]
WHERE [g].[FullName] = [w].[OwnerFullName]
ORDER BY [w].[Id]), 0, 42)
ORDER BY [w].[Id]), 0)
FROM [Gears] AS [g]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ public override async Task String_Join_non_aggregate(bool async)

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 CONCAT_WS(N'|', [c].[CompanyName], COALESCE(@__foo_0, N''), N'', N'bar') = N'Around the Horn|foo||bar'
WHERE CONCAT_WS(N'|', [c].[CompanyName], @__foo_0, N'', N'bar') = N'Around the Horn|foo||bar'
""");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,33 @@ WHERE COALESCE([e].[NullableBoolA], CAST(1 AS bit)) = CAST(1 AS bit)
""");
}

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

AssertSql(
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
WHERE [e].[BoolA] = CAST(1 AS bit) OR [e].[BoolB] = CAST(1 AS bit)
""");
}

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

AssertSql(
"""
SELECT [e].[Id]
FROM [Entities1] AS [e]
WHERE COALESCE([e].[NullableBoolA], CASE
WHEN [e].[BoolA] = CAST(1 AS bit) OR [e].[BoolB] = CAST(1 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END) = CAST(1 AS bit)
""");
}

public override async Task Where_equal_nullable_with_null_value_parameter(bool async)
{
await base.Where_equal_nullable_with_null_value_parameter(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1709,10 +1709,10 @@ public override async Task Select_null_propagation_negative9(bool async)
AssertSql(
"""
SELECT CASE
WHEN [u].[LeaderNickname] IS NOT NULL THEN COALESCE(CASE
WHEN [u].[LeaderNickname] IS NOT NULL THEN CASE
WHEN CAST(LEN([u].[Nickname]) AS int) = 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, CAST(0 AS bit))
END
ELSE NULL
END
FROM (
Expand Down Expand Up @@ -5414,7 +5414,7 @@ public override async Task ToString_enum_property_projection(bool async)
await base.ToString_enum_property_projection(async);

AssertSql(
"""
"""
SELECT CASE [u].[Rank]
WHEN 0 THEN N'None'
WHEN 1 THEN N'Private'
Expand All @@ -5425,7 +5425,7 @@ WHEN 16 THEN N'Captain'
WHEN 32 THEN N'Major'
WHEN 64 THEN N'Colonel'
WHEN 128 THEN N'General'
ELSE COALESCE(CAST([u].[Rank] AS nvarchar(max)), N'')
ELSE CAST([u].[Rank] AS nvarchar(max))
END
FROM (
SELECT [g].[Rank]
Expand Down Expand Up @@ -7861,7 +7861,7 @@ SELECT COALESCE((
SELECT TOP(1) [w].[Id]
FROM [Weapons] AS [w]
WHERE [u].[FullName] = [w].[OwnerFullName]
ORDER BY [w].[Id]), 0, 42)
ORDER BY [w].[Id]), 0)
FROM (
SELECT [g].[FullName]
FROM [Gears] AS [g]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1449,10 +1449,10 @@ public override async Task Select_null_propagation_negative9(bool async)
AssertSql(
"""
SELECT CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN COALESCE(CASE
WHEN [g].[LeaderNickname] IS NOT NULL THEN CASE
WHEN CAST(LEN([g].[Nickname]) AS int) = 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, CAST(0 AS bit))
END
ELSE NULL
END
FROM [Gears] AS [g]
Expand Down Expand Up @@ -4692,7 +4692,7 @@ public override async Task ToString_enum_property_projection(bool async)
await base.ToString_enum_property_projection(async);

AssertSql(
"""
"""
SELECT CASE [g].[Rank]
WHEN 0 THEN N'None'
WHEN 1 THEN N'Private'
Expand All @@ -4703,7 +4703,7 @@ WHEN 16 THEN N'Captain'
WHEN 32 THEN N'Major'
WHEN 64 THEN N'Colonel'
WHEN 128 THEN N'General'
ELSE COALESCE(CAST([g].[Rank] AS nvarchar(max)), N'')
ELSE CAST([g].[Rank] AS nvarchar(max))
END
FROM [Gears] AS [g]
""");
Expand Down Expand Up @@ -6618,7 +6618,7 @@ SELECT COALESCE((
SELECT TOP(1) [w].[Id]
FROM [Weapons] AS [w]
WHERE [g].[FullName] = [w].[OwnerFullName]
ORDER BY [w].[Id]), 0, 42)
ORDER BY [w].[Id]), 0)
FROM [Gears] AS [g]
""");
}
Expand Down
Loading