Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom aggregate translations #28110

Merged
1 commit merged into from
Jul 9, 2022
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 @@ -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