Skip to content

Commit

Permalink
Some custom aggregate translations
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 dotnet#2981
Closes dotnet#28104
  • Loading branch information
roji committed Jun 18, 2022
1 parent f755ffc commit d54a174
Show file tree
Hide file tree
Showing 20 changed files with 1,298 additions and 14 deletions.
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
316 changes: 306 additions & 10 deletions src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs

Large diffs are not rendered by default.

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
@@ -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 SqlFunctionExpression 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 SqlServerSqlFunctionExpression(
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;
}
}
Expand Up @@ -46,4 +46,14 @@ public class SqlServerParameterBasedSqlProcessor : RelationalParameterBasedSqlPr

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);
}
}
31 changes: 31 additions & 0 deletions src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
Expand Up @@ -95,6 +95,37 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression)
}
}

/// <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 VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression)
{
base.VisitSqlFunction(sqlFunctionExpression);

if (sqlFunctionExpression is SqlServerSqlFunctionExpression sqlServerFunctionExpression
&& sqlServerFunctionExpression.AggregateOrderings.Count > 0)
{
Sql.Append(" WITHIN GROUP (ORDER BY ");

for (var i = 0; i < sqlServerFunctionExpression.AggregateOrderings.Count; i++)
{
if (i > 0)
{
Sql.Append(", ");
}

Visit(sqlServerFunctionExpression.AggregateOrderings[i]);
}

Sql.Append(")");
}

return sqlFunctionExpression;
}

/// <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 Down
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
/// </summary>
public class SqlServerSqlExpressionFactory : SqlExpressionFactory
{
private IRelationalTypeMappingSource _typeMappingSource;
private readonly IRelationalTypeMappingSource _typeMappingSource;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
165 changes: 165 additions & 0 deletions src/EFCore.SqlServer/Query/Internal/SqlServerSqlFunctionExpression.cs
@@ -0,0 +1,165 @@
// 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 SqlServerSqlFunctionExpression : SqlFunctionExpression, IEquatable<SqlServerSqlFunctionExpression>
{
/// <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 SqlServerSqlFunctionExpression(
string functionName,
IEnumerable<SqlExpression> arguments,
IReadOnlyList<OrderingExpression> aggregateOrderings,
bool nullable,
IEnumerable<bool> argumentsPropagateNullability,
Type type,
RelationalTypeMapping? typeMapping)
: base(functionName, arguments, nullable, argumentsPropagateNullability, type, typeMapping)
=> AggregateOrderings = aggregateOrderings;

/// <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> AggregateOrderings { 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)
{
var visitedBase = (SqlFunctionExpression)base.VisitChildren(visitor);

OrderingExpression[]? visitedAggregateOrderings = null;

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

for (var j = 0; j < visitedAggregateOrderings.Length; j++)
{
visitedAggregateOrderings[j] = AggregateOrderings[j];
}
}

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

return visitedBase != this || visitedAggregateOrderings is not null
? new SqlServerSqlFunctionExpression(
Name,
visitedBase.Arguments!,
visitedAggregateOrderings ?? AggregateOrderings,
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 override SqlServerSqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
=> new(
Name,
Arguments!,
AggregateOrderings,
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 override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyList<SqlExpression>? arguments)
{
Check.DebugAssert(arguments is not null, "arguments is not null");
Check.DebugAssert(instance is null, "instance not supported on SqlServerFunctionExpression");

return arguments.SequenceEqual(Arguments!)
? this
: new SqlServerSqlFunctionExpression(
Name, arguments, AggregateOrderings, IsNullable, ArgumentsPropagateNullability!, 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
/// 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 SqlFunctionExpression UpdateAggregateOrderings(IReadOnlyList<OrderingExpression> aggregateOrderings)
=> aggregateOrderings.SequenceEqual(AggregateOrderings)
? this
: new SqlServerSqlFunctionExpression(
Name, Arguments!, aggregateOrderings, IsNullable, ArgumentsPropagateNullability!, Type, TypeMapping);

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

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

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

/// <inheritdoc />
public virtual bool Equals(SqlServerSqlFunctionExpression? other)
=> ReferenceEquals(this, other)
|| base.Equals(other) && AggregateOrderings.SequenceEqual(other.AggregateOrderings);

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

hash.Add(base.GetHashCode());

foreach (var orderingExpression in AggregateOrderings)
{
hash.Add(orderingExpression);
}

return hash.ToHashCode();
}
}

0 comments on commit d54a174

Please sign in to comment.