Skip to content

Commit

Permalink
Some custom aggregate translations (#28110)
Browse files Browse the repository at this point in the history
* string.Join (SQL Server and SQLite)
* string.Concat (SQL Server and SQLite)
* Standard deviation and variance (SQL Server)

Closes #2981
Closes #28104
  • Loading branch information
roji committed Jul 9, 2022
1 parent 94d4a1e commit 983112b
Show file tree
Hide file tree
Showing 21 changed files with 1,530 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public QueryableAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFac
averageSqlExpression.Type,
averageSqlExpression.TypeMapping);

// Count/LongCount are special since if the argument is a star fragment, it needs to be transformed to any non-null constant
// when a predicate is applied.
case nameof(Queryable.Count)
when methodInfo == QueryableMethods.CountWithoutPredicate
|| methodInfo == QueryableMethods.CountWithPredicate:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ private SqlFunctionExpression(
/// <summary>
/// A list of bool values indicating whether individual argument propagate null to the result.
/// </summary>

public virtual IReadOnlyList<bool>? ArgumentsPropagateNullability { get; }

/// <inheritdoc />
Expand Down
316 changes: 306 additions & 10 deletions src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class SqlServerAggregateFunctionExpression : SqlExpression
{
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlServerAggregateFunctionExpression(
string name,
IReadOnlyList<SqlExpression> arguments,
IReadOnlyList<OrderingExpression> orderings,
bool nullable,
IEnumerable<bool> argumentsPropagateNullability,
Type type,
RelationalTypeMapping? typeMapping)
: base(type, typeMapping)
{
Name = name;
Arguments = arguments.ToList();
Orderings = orderings;
IsNullable = nullable;
ArgumentsPropagateNullability = argumentsPropagateNullability.ToList();
}

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual string Name { get; }

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IReadOnlyList<SqlExpression> Arguments { get; }

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IReadOnlyList<OrderingExpression> Orderings { get; }

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool IsNullable { get; }

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual IReadOnlyList<bool> ArgumentsPropagateNullability { get; }

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
SqlExpression[]? arguments = null;
for (var i = 0; i < Arguments.Count; i++)
{
var visitedArgument = (SqlExpression)visitor.Visit(Arguments[i]);
if (visitedArgument != Arguments[i] && arguments is null)
{
arguments = new SqlExpression[Arguments.Count];

for (var j = 0; j < i; j++)
{
arguments[j] = Arguments[j];
}
}

if (arguments is not null)
{
arguments[i] = visitedArgument;
}
}

OrderingExpression[]? orderings = null;
for (var i = 0; i < Orderings.Count; i++)
{
var visitedOrdering = (OrderingExpression)visitor.Visit(Orderings[i]);
if (visitedOrdering != Orderings[i] && orderings is null)
{
orderings = new OrderingExpression[Orderings.Count];

for (var j = 0; j < i; j++)
{
orderings[j] = Orderings[j];
}
}

if (orderings is not null)
{
orderings[i] = visitedOrdering;
}
}

return arguments is not null || orderings is not null
? new SqlServerAggregateFunctionExpression(
Name,
arguments ?? Arguments,
orderings ?? Orderings,
IsNullable,
ArgumentsPropagateNullability,
Type,
TypeMapping)
: this;
}

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual SqlServerAggregateFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
=> new(
Name,
Arguments,
Orderings,
IsNullable,
ArgumentsPropagateNullability,
Type,
typeMapping ?? 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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual SqlServerAggregateFunctionExpression Update(
IReadOnlyList<SqlExpression> arguments,
IReadOnlyList<OrderingExpression> orderings)
=> (ReferenceEquals(arguments, Arguments) || arguments.SequenceEqual(Arguments))
&& (ReferenceEquals(orderings, Orderings) || orderings.SequenceEqual(Orderings))
? this
: new SqlServerAggregateFunctionExpression(
Name,
arguments,
orderings,
IsNullable,
ArgumentsPropagateNullability,
Type,
TypeMapping);

/// <inheritdoc />
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append(Name);

expressionPrinter.Append("(");
expressionPrinter.VisitCollection(Arguments);
expressionPrinter.Append(")");

if (Orderings.Count > 0)
{
expressionPrinter.Append(" WITHIN GROUP (ORDER BY ");
expressionPrinter.VisitCollection(Orderings);
expressionPrinter.Append(")");
}
}

/// <inheritdoc />
public override bool Equals(object? obj)
=> obj is SqlServerAggregateFunctionExpression sqlServerFunctionExpression && Equals(sqlServerFunctionExpression);

private bool Equals(SqlServerAggregateFunctionExpression? other)
=> ReferenceEquals(this, other)
|| other is not null
&& base.Equals(other)
&& Name == other.Name
&& Arguments.SequenceEqual(other.Arguments)
&& Orderings.SequenceEqual(other.Orderings);

/// <inheritdoc />
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(base.GetHashCode());
hash.Add(Name);

for (var i = 0; i < Arguments.Count; i++)
{
hash.Add(Arguments[i]);
}

for (var i = 0; i < Orderings.Count; i++)
{
hash.Add(Orderings[i]);
}

return hash.ToHashCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ public SqlServerAggregateMethodCallTranslatorProvider(RelationalAggregateMethodC
: base(dependencies)
{
var sqlExpressionFactory = dependencies.SqlExpressionFactory;
var typeMappingSource = dependencies.RelationalTypeMappingSource;

AddTranslators(
new IAggregateMethodCallTranslator[]
{
new SqlServerLongCountMethodTranslator(sqlExpressionFactory)
new SqlServerLongCountMethodTranslator(sqlExpressionFactory),
new SqlServerStatisticsAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource),
new SqlServerStringAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource)
});
}
}
104 changes: 104 additions & 0 deletions src/EFCore.SqlServer/Query/Internal/SqlServerExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;

/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static class SqlServerExpression
{
/// <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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static SqlFunctionExpression AggregateFunction(
ISqlExpressionFactory sqlExpressionFactory,
string name,
IEnumerable<SqlExpression> arguments,
EnumerableExpression enumerableExpression,
int enumerableArgumentIndex,
bool nullable,
IEnumerable<bool> argumentsPropagateNullability,
Type returnType,
RelationalTypeMapping? typeMapping = null)
=> new(
name,
ProcessAggregateFunctionArguments(sqlExpressionFactory, arguments, enumerableExpression, enumerableArgumentIndex),
nullable,
argumentsPropagateNullability,
returnType,
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
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static SqlExpression AggregateFunctionWithOrdering(
ISqlExpressionFactory sqlExpressionFactory,
string name,
IEnumerable<SqlExpression> arguments,
EnumerableExpression enumerableExpression,
int enumerableArgumentIndex,
bool nullable,
IEnumerable<bool> argumentsPropagateNullability,
Type returnType,
RelationalTypeMapping? typeMapping = null)
=> enumerableExpression.Orderings.Count == 0
? AggregateFunction(sqlExpressionFactory, name, arguments, enumerableExpression, enumerableArgumentIndex, nullable, argumentsPropagateNullability, returnType, typeMapping)
: new SqlServerAggregateFunctionExpression(
name,
ProcessAggregateFunctionArguments(sqlExpressionFactory, arguments, enumerableExpression, enumerableArgumentIndex),
enumerableExpression.Orderings,
nullable,
argumentsPropagateNullability,
returnType,
typeMapping);

private static IReadOnlyList<SqlExpression> ProcessAggregateFunctionArguments(
ISqlExpressionFactory sqlExpressionFactory,
IEnumerable<SqlExpression> arguments,
EnumerableExpression enumerableExpression,
int enumerableArgumentIndex)
{
var argIndex = 0;
var typeMappedArguments = new List<SqlExpression>();

foreach (var argument in arguments)
{
var modifiedArgument = sqlExpressionFactory.ApplyDefaultTypeMapping(argument);

if (argIndex == enumerableArgumentIndex)
{
// This is the argument representing the enumerable inputs to be aggregated.
// Wrap it with a CASE/WHEN for the predicate and with DISTINCT, if necessary.
if (enumerableExpression.Predicate != null)
{
modifiedArgument = sqlExpressionFactory.Case(
new List<CaseWhenClause> { new(enumerableExpression.Predicate, modifiedArgument) },
elseResult: null);
}

if (enumerableExpression.IsDistinct)
{
modifiedArgument = new DistinctExpression(modifiedArgument);
}
}

typeMappedArguments.Add(modifiedArgument);

argIndex++;
}

return typeMappedArguments;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,14 @@ public override Expression Optimize(

return new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(optimizedQueryExpression);
}

/// <inheritdoc />
protected override Expression ProcessSqlNullability(
Expression selectExpression, IReadOnlyDictionary<string, object?> parametersValues, out bool canCache)
{
Check.NotNull(selectExpression, nameof(selectExpression));
Check.NotNull(parametersValues, nameof(parametersValues));

return new SqlServerSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(selectExpression, parametersValues, out canCache);
}
}
Loading

0 comments on commit 983112b

Please sign in to comment.