Skip to content

Commit

Permalink
Work on LEAST/GREATEST, Math.Min/Max (#32338)
Browse files Browse the repository at this point in the history
Closes #27794
Closes #31681
Closes #32332
  • Loading branch information
roji authored Nov 28, 2023
1 parent 484a3ed commit d79cd19
Show file tree
Hide file tree
Showing 28 changed files with 896 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,24 @@ public static TProperty Collate<TProperty>(
TProperty operand,
[NotParameterized] string collation)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Collate)));

/// <summary>
/// Returns the smallest value from the given list of values. Usually corresponds to the <c>LEAST</c> SQL function.
/// </summary>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="values">The list of values from which return the smallest value.</param>
public static T Least<T>(
this DbFunctions _,
[NotParameterized] params T[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Least)));

/// <summary>
/// Returns the greatest value from the given list of values. Usually corresponds to the <c>GREATEST</c> SQL function.
/// </summary>
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
/// <param name="values">The list of values from which return the greatest value.</param>
public static T Greatest<T>(
this DbFunctions _,
[NotParameterized] params T[] values)
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Greatest)));
}
19 changes: 19 additions & 0 deletions src/EFCore.Relational/Query/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,23 @@ public static class ExpressionExtensions

return null;
}

/// <summary>
/// Infers type mapping from given <see cref="SqlExpression" />s.
/// </summary>
/// <param name="expressions">Expressions to search for to find the type mapping.</param>
/// <returns>A relational type mapping inferred from the expressions.</returns>
public static RelationalTypeMapping? InferTypeMapping(IReadOnlyList<SqlExpression> expressions)
{
for (var i = 0; i < expressions.Count; i++)
{
var sql = expressions[i];
if (sql.TypeMapping != null)
{
return sql.TypeMapping;
}
}

return null;
}
}
26 changes: 26 additions & 0 deletions src/EFCore.Relational/Query/ISqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -483,4 +483,30 @@ SqlFunctionExpression NiladicFunction(
/// <param name="tableExpressionBase">A table source to project from.</param>
/// <returns>An expression representing a SELECT in a SQL tree.</returns>
SelectExpression Select(IEntityType entityType, TableExpressionBase tableExpressionBase);

/// <summary>
/// Attempts to creates a new expression that returns the smallest value from a list of expressions, e.g. an invocation of the
/// <c>LEAST</c> SQL function.
/// </summary>
/// <param name="expressions">An entity type to project.</param>
/// <param name="resultType">The result CLR type for the returned expression.</param>
/// <param name="leastExpression">The expression which computes the smallest value.</param>
/// <returns><see langword="true" /> if the expression could be created, <see langword="false" /> otherwise.</returns>
bool TryCreateLeast(
IReadOnlyList<SqlExpression> expressions,
Type resultType,
[NotNullWhen(true)] out SqlExpression? leastExpression);

/// <summary>
/// Attempts to creates a new expression that returns the greatest value from a list of expressions, e.g. an invocation of the
/// <c>GREATEST</c> SQL function.
/// </summary>
/// <param name="expressions">An entity type to project.</param>
/// <param name="resultType">The result CLR type for the returned expression.</param>
/// <param name="greatestExpression">The expression which computes the greatest value.</param>
/// <returns><see langword="true" /> if the expression could be created, <see langword="false" /> otherwise.</returns>
bool TryCreateGreatest(
IReadOnlyList<SqlExpression> expressions,
Type resultType,
[NotNullWhen(true)] out SqlExpression? greatestExpression);
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
if (translated == QueryCompilationContext.NotTranslatedExpression)
{
// Attempt to translate access into a primitive collection property (i.e. array column)

if (_sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var translatedExpression, out var property)
&& property is IProperty { IsPrimitiveCollection: true } regularProperty
&& translatedExpression is SqlExpression sqlExpression)
Expand Down Expand Up @@ -564,31 +563,8 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s
}

// Pattern-match Contains over ValuesExpression, translating to simplified 'item IN (1, 2, 3)' with constant elements
if (source.QueryExpression is SelectExpression
{
Tables:
[
ValuesExpression { ColumnNames: [ValuesOrderingColumnName, ValuesValueColumnName] } valuesExpression
],
Predicate: null,
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null,
// Note that in the context of Contains we don't care about orderings
}
// Make sure that the source projects the column from the ValuesExpression directly, i.e. no projection out with some expression
&& projection is ColumnExpression projectedColumn
&& projectedColumn.Table == valuesExpression)
if (TryExtractBareInlineCollectionValues(source, out var values))
{
var values = new SqlExpression[valuesExpression.RowValues.Count];
for (var i = 0; i < values.Length; i++)
{
// Skip the first value (_ord), which is irrelevant for Contains
values[i] = valuesExpression.RowValues[i].Values[1];
}

var inExpression = _sqlExpressionFactory.In(translatedItem, values);
return source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression);
}
Expand Down Expand Up @@ -960,13 +936,19 @@ private SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerK

/// <inheritdoc />
protected override ShapedQueryExpression? TranslateMax(ShapedQueryExpression source, LambdaExpression? selector, Type resultType)
=> TranslateAggregateWithSelector(
source, selector, t => QueryableMethods.MaxWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);
=> TryExtractBareInlineCollectionValues(source, out var values)
&& _sqlExpressionFactory.TryCreateGreatest(values, resultType, out var greatestExpression)
? source.Update(_sqlExpressionFactory.Select(greatestExpression), source.ShaperExpression)
: TranslateAggregateWithSelector(
source, selector, t => QueryableMethods.MaxWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);

/// <inheritdoc />
protected override ShapedQueryExpression? TranslateMin(ShapedQueryExpression source, LambdaExpression? selector, Type resultType)
=> TranslateAggregateWithSelector(
source, selector, t => QueryableMethods.MinWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);
=> TryExtractBareInlineCollectionValues(source, out var values)
&& _sqlExpressionFactory.TryCreateLeast(values, resultType, out var leastExpression)
? source.Update(_sqlExpressionFactory.Select(leastExpression), source.ShaperExpression)
: TranslateAggregateWithSelector(
source, selector, t => QueryableMethods.MinWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);

/// <inheritdoc />
protected override ShapedQueryExpression? TranslateOfType(ShapedQueryExpression source, Type resultType)
Expand Down Expand Up @@ -2602,6 +2584,42 @@ private bool TryGetProjection(ShapedQueryExpression shapedQueryExpression, [NotN
return false;
}

private bool TryExtractBareInlineCollectionValues(ShapedQueryExpression shapedQuery, [NotNullWhen(true)] out SqlExpression[]? values)
{
if (TryGetProjection(shapedQuery, out var projection)
&& shapedQuery.QueryExpression is SelectExpression
{
Tables:
[
ValuesExpression { ColumnNames: [ValuesOrderingColumnName, ValuesValueColumnName] } valuesExpression
],
Predicate: null,
GroupBy: [],
Having: null,
IsDistinct: false,
Limit: null,
Offset: null,
// Note that we assume ordering doesn't matter (Contains/Min/Max)
}
// Make sure that the source projects the column from the ValuesExpression directly, i.e. no projection out with some expression
&& projection is ColumnExpression projectedColumn
&& projectedColumn.Table == valuesExpression)
{
values = new SqlExpression[valuesExpression.RowValues.Count];

for (var i = 0; i < values.Length; i++)
{
// Skip the first value (_ord) - this function assumes ordering doesn't matter
values[i] = valuesExpression.RowValues[i].Values[1];
}

return true;
}

values = null;
return false;
}

/// <summary>
/// A visitor which scans an expression tree and attempts to find columns for which we were missing type mappings (projected out
/// of queryable constant/parameter), and those type mappings have been inferred.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ private static readonly MethodInfo StringEqualsWithStringComparison
private static readonly MethodInfo StringEqualsWithStringComparisonStatic
= typeof(string).GetRuntimeMethod(nameof(string.Equals), new[] { typeof(string), typeof(string), typeof(StringComparison) })!;

private static readonly MethodInfo LeastMethodInfo
= typeof(RelationalDbFunctionsExtensions).GetMethod(nameof(RelationalDbFunctionsExtensions.Least))!;

private static readonly MethodInfo GreatestMethodInfo
= typeof(RelationalDbFunctionsExtensions).GetMethod(nameof(RelationalDbFunctionsExtensions.Greatest))!;

private static readonly MethodInfo GetTypeMethodInfo = typeof(object).GetTypeInfo().GetDeclaredMethod(nameof(GetType))!;

private readonly QueryCompilationContext _queryCompilationContext;
Expand Down Expand Up @@ -932,6 +938,47 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return QueryCompilationContext.NotTranslatedExpression;
}
}
// Translate EF.Functions.Greatest/Least.
// These are here rather than in a MethodTranslator since the parameter is an array, and that's not supported in regular
// translation.
else if (method.DeclaringType == typeof(RelationalDbFunctionsExtensions)
&& method.IsGenericMethod
&& method.GetGenericMethodDefinition() is var genericMethodDefinition
&& (genericMethodDefinition == LeastMethodInfo || genericMethodDefinition == GreatestMethodInfo)
&& methodCallExpression.Arguments[1] is NewArrayExpression newArray)
{
var values = newArray.Expressions;
var translatedValues = new SqlExpression[values.Count];

for (var i = 0; i < values.Count; i++)
{
var value = values[i];
var visitedValue = Visit(value);

if (TranslationFailed(value, visitedValue, out var translatedValue))
{
return QueryCompilationContext.NotTranslatedExpression;
}

translatedValues[i] = translatedValue!;
}

var elementClrType = newArray.Type.GetElementType()!;

if (genericMethodDefinition == LeastMethodInfo
&& _sqlExpressionFactory.TryCreateLeast(translatedValues, elementClrType, out var leastExpression))
{
return leastExpression;
}

if (genericMethodDefinition == GreatestMethodInfo
&& _sqlExpressionFactory.TryCreateGreatest(translatedValues, elementClrType, out var greatestExpression))
{
return greatestExpression;
}

throw new UnreachableException();
}
else
{
if (method.IsStatic
Expand Down
61 changes: 61 additions & 0 deletions src/EFCore.Relational/Query/SqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,67 @@ public virtual SelectExpression Select(IEntityType entityType, TableExpressionBa
return selectExpression;
}

/// <inheritdoc />
public virtual bool TryCreateLeast(
IReadOnlyList<SqlExpression> expressions,
Type resultType,
[NotNullWhen(true)] out SqlExpression? leastExpression)
{
var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions);

expressions = FlattenLeastGreatest("LEAST", expressions);

leastExpression = Function(
"LEAST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping);
return true;
}

/// <inheritdoc />
public virtual bool TryCreateGreatest(
IReadOnlyList<SqlExpression> expressions,
Type resultType,
[NotNullWhen(true)] out SqlExpression? greatestExpression)
{
var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions);

expressions = FlattenLeastGreatest("GREATEST", expressions);

greatestExpression = Function(
"GREATEST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping);
return true;
}

private IReadOnlyList<SqlExpression> FlattenLeastGreatest(string functionName, IReadOnlyList<SqlExpression> expressions)
{
List<SqlExpression>? flattenedExpressions = null;

for (var i = 0; i < expressions.Count; i++)
{
var expression = expressions[i];
if (expression is SqlFunctionExpression { IsBuiltIn: true } nestedFunction
&& nestedFunction.Name == functionName)
{
if (flattenedExpressions is null)
{
flattenedExpressions = new List<SqlExpression>();
for (var j = 0; j < i; j++)
{
flattenedExpressions.Add(expressions[j]);
}
}

Check.DebugAssert(nestedFunction.Arguments is not null, "Null arguments to " + functionName);
flattenedExpressions.AddRange(nestedFunction.Arguments);
}
else
{
flattenedExpressions?.Add(expressions[i]);
}
}

return flattenedExpressions ?? expressions;
}

/***
* We need to add additional conditions on basic SelectExpression for certain cases
* - If we are selecting from TPH then we need to add condition for discriminator if mapping is incomplete
Expand Down
6 changes: 6 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@
<data name="DuplicateKeyMismatchedClustering" xml:space="preserve">
<value>The keys {key1} on '{entityType1}' and {key2} on '{entityType2}' are both mapped to '{table}.{keyName}', but have different clustering configurations.</value>
</data>
<data name="LeastGreatestCompatibilityLevelTooLow" xml:space="preserve">
<value>This usage of Math.Min or Math.Max requires SQL Server functions LEAST and GREATEST, which require compatibility level 160.</value>
</data>
<data name="IdentityBadType" xml:space="preserve">
<value>Identity value generation cannot be used for the property '{property}' on entity type '{entityType}' because the property type is '{propertyType}'. Identity value generation can only be used with signed integer properties.</value>
</data>
Expand Down
27 changes: 27 additions & 0 deletions src/EFCore.SqlServer/Query/Internal/SqlServerMathTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;

namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
Expand Down Expand Up @@ -186,6 +188,31 @@ public SqlServerMathTranslator(ISqlExpressionFactory sqlExpressionFactory)
return _sqlExpressionFactory.ApplyTypeMapping(result, argument.TypeMapping);
}

if (method.DeclaringType == typeof(Math))
{
if (method.Name == nameof(Math.Min))
{
if (_sqlExpressionFactory.TryCreateLeast(
new[] { arguments[0], arguments[1] }, method.ReturnType, out var leastExpression))
{
return leastExpression;
}

throw new InvalidOperationException(SqlServerStrings.LeastGreatestCompatibilityLevelTooLow);
}

if (method.Name == nameof(Math.Max))
{
if (_sqlExpressionFactory.TryCreateGreatest(
new[] { arguments[0], arguments[1] }, method.ReturnType, out var leastExpression))
{
return leastExpression;
}

throw new InvalidOperationException(SqlServerStrings.LeastGreatestCompatibilityLevelTooLow);
}
}

return null;
}
}
Loading

0 comments on commit d79cd19

Please sign in to comment.