Skip to content

Commit d54a174

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 d54a174

20 files changed

+1298
-14
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.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs

Lines changed: 306 additions & 10 deletions
Large diffs are not rendered by default.

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 SqlFunctionExpression 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 SqlServerSqlFunctionExpression(
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
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,37 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression)
9595
}
9696
}
9797

98+
/// <summary>
99+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
100+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
101+
/// any release. You should only use it directly in your code with extreme caution and knowing that
102+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
103+
/// </summary>
104+
protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression)
105+
{
106+
base.VisitSqlFunction(sqlFunctionExpression);
107+
108+
if (sqlFunctionExpression is SqlServerSqlFunctionExpression sqlServerFunctionExpression
109+
&& sqlServerFunctionExpression.AggregateOrderings.Count > 0)
110+
{
111+
Sql.Append(" WITHIN GROUP (ORDER BY ");
112+
113+
for (var i = 0; i < sqlServerFunctionExpression.AggregateOrderings.Count; i++)
114+
{
115+
if (i > 0)
116+
{
117+
Sql.Append(", ");
118+
}
119+
120+
Visit(sqlServerFunctionExpression.AggregateOrderings[i]);
121+
}
122+
123+
Sql.Append(")");
124+
}
125+
126+
return sqlFunctionExpression;
127+
}
128+
98129
/// <summary>
99130
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
100131
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
1414
/// </summary>
1515
public class SqlServerSqlExpressionFactory : SqlExpressionFactory
1616
{
17-
private IRelationalTypeMappingSource _typeMappingSource;
17+
private readonly IRelationalTypeMappingSource _typeMappingSource;
1818

1919
/// <summary>
2020
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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 SqlServerSqlFunctionExpression : SqlFunctionExpression, IEquatable<SqlServerSqlFunctionExpression>
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 SqlServerSqlFunctionExpression(
23+
string functionName,
24+
IEnumerable<SqlExpression> arguments,
25+
IReadOnlyList<OrderingExpression> aggregateOrderings,
26+
bool nullable,
27+
IEnumerable<bool> argumentsPropagateNullability,
28+
Type type,
29+
RelationalTypeMapping? typeMapping)
30+
: base(functionName, arguments, nullable, argumentsPropagateNullability, type, typeMapping)
31+
=> AggregateOrderings = aggregateOrderings;
32+
33+
/// <summary>
34+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
35+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
36+
/// any release. You should only use it directly in your code with extreme caution and knowing that
37+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
38+
/// </summary>
39+
public virtual IReadOnlyList<OrderingExpression> AggregateOrderings { get; }
40+
41+
/// <summary>
42+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
43+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
44+
/// any release. You should only use it directly in your code with extreme caution and knowing that
45+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
46+
/// </summary>
47+
protected override Expression VisitChildren(ExpressionVisitor visitor)
48+
{
49+
var visitedBase = (SqlFunctionExpression)base.VisitChildren(visitor);
50+
51+
OrderingExpression[]? visitedAggregateOrderings = null;
52+
53+
for (var i = 0; i < AggregateOrderings.Count; i++)
54+
{
55+
var visitedOrdering = (OrderingExpression)visitor.Visit(AggregateOrderings[i]);
56+
if (visitedOrdering != AggregateOrderings[i] && visitedAggregateOrderings is null)
57+
{
58+
visitedAggregateOrderings = new OrderingExpression[AggregateOrderings.Count];
59+
60+
for (var j = 0; j < visitedAggregateOrderings.Length; j++)
61+
{
62+
visitedAggregateOrderings[j] = AggregateOrderings[j];
63+
}
64+
}
65+
66+
if (visitedAggregateOrderings is not null)
67+
{
68+
visitedAggregateOrderings[i] = visitedOrdering;
69+
}
70+
}
71+
72+
return visitedBase != this || visitedAggregateOrderings is not null
73+
? new SqlServerSqlFunctionExpression(
74+
Name,
75+
visitedBase.Arguments!,
76+
visitedAggregateOrderings ?? AggregateOrderings,
77+
IsNullable,
78+
ArgumentsPropagateNullability!,
79+
Type,
80+
TypeMapping)
81+
: this;
82+
}
83+
84+
/// <summary>
85+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
86+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
87+
/// any release. You should only use it directly in your code with extreme caution and knowing that
88+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
89+
/// </summary>
90+
public override SqlServerSqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
91+
=> new(
92+
Name,
93+
Arguments!,
94+
AggregateOrderings,
95+
IsNullable,
96+
ArgumentsPropagateNullability!,
97+
Type,
98+
typeMapping ?? TypeMapping);
99+
100+
/// <summary>
101+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
102+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
103+
/// any release. You should only use it directly in your code with extreme caution and knowing that
104+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
105+
/// </summary>
106+
public override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyList<SqlExpression>? arguments)
107+
{
108+
Check.DebugAssert(arguments is not null, "arguments is not null");
109+
Check.DebugAssert(instance is null, "instance not supported on SqlServerFunctionExpression");
110+
111+
return arguments.SequenceEqual(Arguments!)
112+
? this
113+
: new SqlServerSqlFunctionExpression(
114+
Name, arguments, AggregateOrderings, IsNullable, ArgumentsPropagateNullability!, Type, TypeMapping);
115+
}
116+
117+
/// <summary>
118+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
119+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
120+
/// any release. You should only use it directly in your code with extreme caution and knowing that
121+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
122+
/// </summary>
123+
public virtual SqlFunctionExpression UpdateAggregateOrderings(IReadOnlyList<OrderingExpression> aggregateOrderings)
124+
=> aggregateOrderings.SequenceEqual(AggregateOrderings)
125+
? this
126+
: new SqlServerSqlFunctionExpression(
127+
Name, Arguments!, aggregateOrderings, IsNullable, ArgumentsPropagateNullability!, Type, TypeMapping);
128+
129+
/// <inheritdoc />
130+
protected override void Print(ExpressionPrinter expressionPrinter)
131+
{
132+
base.Print(expressionPrinter);
133+
134+
if (AggregateOrderings.Count > 0)
135+
{
136+
expressionPrinter.Append(" WITHIN GROUP (ORDER BY ");
137+
expressionPrinter.VisitCollection(AggregateOrderings);
138+
expressionPrinter.Append(")");
139+
}
140+
}
141+
142+
/// <inheritdoc />
143+
public override bool Equals(object? obj)
144+
=> obj is SqlServerSqlFunctionExpression sqlServerFunctionExpression && Equals(sqlServerFunctionExpression);
145+
146+
/// <inheritdoc />
147+
public virtual bool Equals(SqlServerSqlFunctionExpression? other)
148+
=> ReferenceEquals(this, other)
149+
|| base.Equals(other) && AggregateOrderings.SequenceEqual(other.AggregateOrderings);
150+
151+
/// <inheritdoc />
152+
public override int GetHashCode()
153+
{
154+
var hash = new HashCode();
155+
156+
hash.Add(base.GetHashCode());
157+
158+
foreach (var orderingExpression in AggregateOrderings)
159+
{
160+
hash.Add(orderingExpression);
161+
}
162+
163+
return hash.ToHashCode();
164+
}
165+
}

0 commit comments

Comments
 (0)