Skip to content

Commit 578fd86

Browse files
committed
Some custom aggregate translations
* string.Join (SQL Server and SQLite) * string.Concat (SQL Server and SQLite) * Standard deviation and variance (SQL Server) Closes #2981 Closes #28104
1 parent f755ffc commit 578fd86

21 files changed

+1530
-73
lines changed

src/EFCore.Relational/Query/Internal/QueryableAggregateMethodTranslator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public QueryableAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFac
7474
averageSqlExpression.Type,
7575
averageSqlExpression.TypeMapping);
7676

77+
// Count/LongCount are special since if the argument is a star fragment, it needs to be transformed to any non-null constant
78+
// when a predicate is applied.
7779
case nameof(Queryable.Count)
7880
when methodInfo == QueryableMethods.CountWithoutPredicate
7981
|| methodInfo == QueryableMethods.CountWithPredicate:

src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,6 @@ private SqlFunctionExpression(
250250
/// <summary>
251251
/// A list of bool values indicating whether individual argument propagate null to the result.
252252
/// </summary>
253-
254253
public virtual IReadOnlyList<bool>? ArgumentsPropagateNullability { get; }
255254

256255
/// <inheritdoc />

src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs

Lines changed: 306 additions & 10 deletions
Large diffs are not rendered by default.
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
5+
6+
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
7+
8+
/// <summary>
9+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
10+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
11+
/// any release. You should only use it directly in your code with extreme caution and knowing that
12+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
13+
/// </summary>
14+
public class SqlServerAggregateFunctionExpression : SqlExpression
15+
{
16+
/// <summary>
17+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
18+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
19+
/// any release. You should only use it directly in your code with extreme caution and knowing that
20+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
21+
/// </summary>
22+
public SqlServerAggregateFunctionExpression(
23+
string name,
24+
IReadOnlyList<SqlExpression> arguments,
25+
IReadOnlyList<OrderingExpression> orderings,
26+
bool nullable,
27+
IEnumerable<bool> argumentsPropagateNullability,
28+
Type type,
29+
RelationalTypeMapping? typeMapping)
30+
: base(type, typeMapping)
31+
{
32+
Name = name;
33+
Arguments = arguments.ToList();
34+
Orderings = orderings;
35+
IsNullable = nullable;
36+
ArgumentsPropagateNullability = argumentsPropagateNullability.ToList();
37+
}
38+
39+
/// <summary>
40+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
41+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
42+
/// any release. You should only use it directly in your code with extreme caution and knowing that
43+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
44+
/// </summary>
45+
public virtual string Name { get; }
46+
47+
/// <summary>
48+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
49+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
50+
/// any release. You should only use it directly in your code with extreme caution and knowing that
51+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
52+
/// </summary>
53+
public virtual IReadOnlyList<SqlExpression> Arguments { get; }
54+
55+
/// <summary>
56+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
57+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
58+
/// any release. You should only use it directly in your code with extreme caution and knowing that
59+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
60+
/// </summary>
61+
public virtual IReadOnlyList<OrderingExpression> Orderings { get; }
62+
63+
/// <summary>
64+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
65+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
66+
/// any release. You should only use it directly in your code with extreme caution and knowing that
67+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
68+
/// </summary>
69+
public virtual bool IsNullable { get; }
70+
71+
/// <summary>
72+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
73+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
74+
/// any release. You should only use it directly in your code with extreme caution and knowing that
75+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
76+
/// </summary>
77+
public virtual IReadOnlyList<bool> ArgumentsPropagateNullability { get; }
78+
79+
/// <summary>
80+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
81+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
82+
/// any release. You should only use it directly in your code with extreme caution and knowing that
83+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
84+
/// </summary>
85+
protected override Expression VisitChildren(ExpressionVisitor visitor)
86+
{
87+
SqlExpression[]? arguments = null;
88+
for (var i = 0; i < Arguments.Count; i++)
89+
{
90+
var visitedArgument = (SqlExpression)visitor.Visit(Arguments[i]);
91+
if (visitedArgument != Arguments[i] && arguments is null)
92+
{
93+
arguments = new SqlExpression[Arguments.Count];
94+
95+
for (var j = 0; j < i; j++)
96+
{
97+
arguments[j] = Arguments[j];
98+
}
99+
}
100+
101+
if (arguments is not null)
102+
{
103+
arguments[i] = visitedArgument;
104+
}
105+
}
106+
107+
OrderingExpression[]? orderings = null;
108+
for (var i = 0; i < Orderings.Count; i++)
109+
{
110+
var visitedOrdering = (OrderingExpression)visitor.Visit(Orderings[i]);
111+
if (visitedOrdering != Orderings[i] && orderings is null)
112+
{
113+
orderings = new OrderingExpression[Orderings.Count];
114+
115+
for (var j = 0; j < i; j++)
116+
{
117+
orderings[j] = Orderings[j];
118+
}
119+
}
120+
121+
if (orderings is not null)
122+
{
123+
orderings[i] = visitedOrdering;
124+
}
125+
}
126+
127+
return arguments is not null || orderings is not null
128+
? new SqlServerAggregateFunctionExpression(
129+
Name,
130+
arguments ?? Arguments,
131+
orderings ?? Orderings,
132+
IsNullable,
133+
ArgumentsPropagateNullability,
134+
Type,
135+
TypeMapping)
136+
: this;
137+
}
138+
139+
/// <summary>
140+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
141+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
142+
/// any release. You should only use it directly in your code with extreme caution and knowing that
143+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
144+
/// </summary>
145+
public virtual SqlServerAggregateFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
146+
=> new(
147+
Name,
148+
Arguments,
149+
Orderings,
150+
IsNullable,
151+
ArgumentsPropagateNullability,
152+
Type,
153+
typeMapping ?? TypeMapping);
154+
155+
/// <summary>
156+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
157+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
158+
/// any release. You should only use it directly in your code with extreme caution and knowing that
159+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
160+
/// </summary>
161+
public virtual SqlServerAggregateFunctionExpression Update(
162+
IReadOnlyList<SqlExpression> arguments,
163+
IReadOnlyList<OrderingExpression> orderings)
164+
=> (ReferenceEquals(arguments, Arguments) || arguments.SequenceEqual(Arguments))
165+
&& (ReferenceEquals(orderings, Orderings) || orderings.SequenceEqual(Orderings))
166+
? this
167+
: new SqlServerAggregateFunctionExpression(
168+
Name,
169+
arguments,
170+
orderings,
171+
IsNullable,
172+
ArgumentsPropagateNullability,
173+
Type,
174+
TypeMapping);
175+
176+
/// <inheritdoc />
177+
protected override void Print(ExpressionPrinter expressionPrinter)
178+
{
179+
expressionPrinter.Append(Name);
180+
181+
expressionPrinter.Append("(");
182+
expressionPrinter.VisitCollection(Arguments);
183+
expressionPrinter.Append(")");
184+
185+
if (Orderings.Count > 0)
186+
{
187+
expressionPrinter.Append(" WITHIN GROUP (ORDER BY ");
188+
expressionPrinter.VisitCollection(Orderings);
189+
expressionPrinter.Append(")");
190+
}
191+
}
192+
193+
/// <inheritdoc />
194+
public override bool Equals(object? obj)
195+
=> obj is SqlServerAggregateFunctionExpression sqlServerFunctionExpression && Equals(sqlServerFunctionExpression);
196+
197+
private bool Equals(SqlServerAggregateFunctionExpression? other)
198+
=> ReferenceEquals(this, other)
199+
|| other is not null
200+
&& base.Equals(other)
201+
&& Name == other.Name
202+
&& Arguments.SequenceEqual(other.Arguments)
203+
&& Orderings.SequenceEqual(other.Orderings);
204+
205+
/// <inheritdoc />
206+
public override int GetHashCode()
207+
{
208+
var hash = new HashCode();
209+
hash.Add(base.GetHashCode());
210+
hash.Add(Name);
211+
212+
for (var i = 0; i < Arguments.Count; i++)
213+
{
214+
hash.Add(Arguments[i]);
215+
}
216+
217+
for (var i = 0; i < Orderings.Count; i++)
218+
{
219+
hash.Add(Orderings[i]);
220+
}
221+
222+
return hash.ToHashCode();
223+
}
224+
}

src/EFCore.SqlServer/Query/Internal/SqlServerAggregateMethodCallTranslatorProvider.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ public SqlServerAggregateMethodCallTranslatorProvider(RelationalAggregateMethodC
2121
: base(dependencies)
2222
{
2323
var sqlExpressionFactory = dependencies.SqlExpressionFactory;
24+
var typeMappingSource = dependencies.RelationalTypeMappingSource;
25+
2426
AddTranslators(
2527
new IAggregateMethodCallTranslator[]
2628
{
27-
new SqlServerLongCountMethodTranslator(sqlExpressionFactory)
29+
new SqlServerLongCountMethodTranslator(sqlExpressionFactory),
30+
new SqlServerStatisticsAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource),
31+
new SqlServerStringAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource)
2832
});
2933
}
3034
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
5+
6+
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
7+
8+
/// <summary>
9+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
10+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
11+
/// any release. You should only use it directly in your code with extreme caution and knowing that
12+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
13+
/// </summary>
14+
public static class SqlServerExpression
15+
{
16+
/// <summary>
17+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
18+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
19+
/// any release. You should only use it directly in your code with extreme caution and knowing that
20+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
21+
/// </summary>
22+
public static SqlFunctionExpression AggregateFunction(
23+
ISqlExpressionFactory sqlExpressionFactory,
24+
string name,
25+
IEnumerable<SqlExpression> arguments,
26+
EnumerableExpression enumerableExpression,
27+
int enumerableArgumentIndex,
28+
bool nullable,
29+
IEnumerable<bool> argumentsPropagateNullability,
30+
Type returnType,
31+
RelationalTypeMapping? typeMapping = null)
32+
=> new(
33+
name,
34+
ProcessAggregateFunctionArguments(sqlExpressionFactory, arguments, enumerableExpression, enumerableArgumentIndex),
35+
nullable,
36+
argumentsPropagateNullability,
37+
returnType,
38+
typeMapping);
39+
40+
/// <summary>
41+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
42+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
43+
/// any release. You should only use it directly in your code with extreme caution and knowing that
44+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
45+
/// </summary>
46+
public static SqlExpression AggregateFunctionWithOrdering(
47+
ISqlExpressionFactory sqlExpressionFactory,
48+
string name,
49+
IEnumerable<SqlExpression> arguments,
50+
EnumerableExpression enumerableExpression,
51+
int enumerableArgumentIndex,
52+
bool nullable,
53+
IEnumerable<bool> argumentsPropagateNullability,
54+
Type returnType,
55+
RelationalTypeMapping? typeMapping = null)
56+
=> enumerableExpression.Orderings.Count == 0
57+
? AggregateFunction(sqlExpressionFactory, name, arguments, enumerableExpression, enumerableArgumentIndex, nullable, argumentsPropagateNullability, returnType, typeMapping)
58+
: new SqlServerAggregateFunctionExpression(
59+
name,
60+
ProcessAggregateFunctionArguments(sqlExpressionFactory, arguments, enumerableExpression, enumerableArgumentIndex),
61+
enumerableExpression.Orderings,
62+
nullable,
63+
argumentsPropagateNullability,
64+
returnType,
65+
typeMapping);
66+
67+
private static IReadOnlyList<SqlExpression> ProcessAggregateFunctionArguments(
68+
ISqlExpressionFactory sqlExpressionFactory,
69+
IEnumerable<SqlExpression> arguments,
70+
EnumerableExpression enumerableExpression,
71+
int enumerableArgumentIndex)
72+
{
73+
var argIndex = 0;
74+
var typeMappedArguments = new List<SqlExpression>();
75+
76+
foreach (var argument in arguments)
77+
{
78+
var modifiedArgument = sqlExpressionFactory.ApplyDefaultTypeMapping(argument);
79+
80+
if (argIndex == enumerableArgumentIndex)
81+
{
82+
// This is the argument representing the enumerable inputs to be aggregated.
83+
// Wrap it with a CASE/WHEN for the predicate and with DISTINCT, if necessary.
84+
if (enumerableExpression.Predicate != null)
85+
{
86+
modifiedArgument = sqlExpressionFactory.Case(
87+
new List<CaseWhenClause> { new(enumerableExpression.Predicate, modifiedArgument) },
88+
elseResult: null);
89+
}
90+
91+
if (enumerableExpression.IsDistinct)
92+
{
93+
modifiedArgument = new DistinctExpression(modifiedArgument);
94+
}
95+
}
96+
97+
typeMappedArguments.Add(modifiedArgument);
98+
99+
argIndex++;
100+
}
101+
102+
return typeMappedArguments;
103+
}
104+
}

src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,14 @@ public override Expression Optimize(
4646

4747
return new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(optimizedQueryExpression);
4848
}
49+
50+
/// <inheritdoc />
51+
protected override Expression ProcessSqlNullability(
52+
Expression selectExpression, IReadOnlyDictionary<string, object?> parametersValues, out bool canCache)
53+
{
54+
Check.NotNull(selectExpression, nameof(selectExpression));
55+
Check.NotNull(parametersValues, nameof(parametersValues));
56+
57+
return new SqlServerSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(selectExpression, parametersValues, out canCache);
58+
}
4959
}

0 commit comments

Comments
 (0)