Skip to content

Commit a85f8ed

Browse files
committed
Work on LEAST/GREATEST, Math.Min/Max
Closes dotnet#27794 Closes dotnet#31681 Closes dotnet#32332
1 parent f55b4a0 commit a85f8ed

25 files changed

+645
-59
lines changed

src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs

+20
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,24 @@ public static TProperty Collate<TProperty>(
3636
TProperty operand,
3737
[NotParameterized] string collation)
3838
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Collate)));
39+
40+
/// <summary>
41+
/// Returns the smallest value from the given list of values. Usually corresponds to the <c>LEAST</c> SQL function.
42+
/// </summary>
43+
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
44+
/// <param name="values">The list of values from which return the smallest value.</param>
45+
public static T Least<T>(
46+
this DbFunctions _,
47+
[NotParameterized] params T[] values)
48+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Least)));
49+
50+
/// <summary>
51+
/// Returns the greatest value from the given list of values. Usually corresponds to the <c>GREATEST</c> SQL function.
52+
/// </summary>
53+
/// <param name="_">The <see cref="DbFunctions" /> instance.</param>
54+
/// <param name="values">The list of values from which return the greatest value.</param>
55+
public static T Greatest<T>(
56+
this DbFunctions _,
57+
[NotParameterized] params T[] values)
58+
=> throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Greatest)));
3959
}

src/EFCore.Relational/Query/ExpressionExtensions.cs

+19
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,23 @@ public static class ExpressionExtensions
3434

3535
return null;
3636
}
37+
38+
/// <summary>
39+
/// Infers type mapping from given <see cref="SqlExpression" />s.
40+
/// </summary>
41+
/// <param name="expressions">Expressions to search for to find the type mapping.</param>
42+
/// <returns>A relational type mapping inferred from the expressions.</returns>
43+
public static RelationalTypeMapping? InferTypeMapping(IReadOnlyList<SqlExpression> expressions)
44+
{
45+
for (var i = 0; i < expressions.Count; i++)
46+
{
47+
var sql = expressions[i];
48+
if (sql.TypeMapping != null)
49+
{
50+
return sql.TypeMapping;
51+
}
52+
}
53+
54+
return null;
55+
}
3756
}

src/EFCore.Relational/Query/ISqlExpressionFactory.cs

+26
Original file line numberDiff line numberDiff line change
@@ -483,4 +483,30 @@ SqlFunctionExpression NiladicFunction(
483483
/// <param name="tableExpressionBase">A table source to project from.</param>
484484
/// <returns>An expression representing a SELECT in a SQL tree.</returns>
485485
SelectExpression Select(IEntityType entityType, TableExpressionBase tableExpressionBase);
486+
487+
/// <summary>
488+
/// Attempts to creates a new expression that returns the smallest value from a list of expressions, e.g. an invocation of the
489+
/// <c>LEAST</c> SQL function.
490+
/// </summary>
491+
/// <param name="expressions">An entity type to project.</param>
492+
/// <param name="resultType">The result CLR type for the returned expression.</param>
493+
/// <param name="leastExpression">The expression which computes the smallest value.</param>
494+
/// <returns><see langword="true" /> if the expression could be created, <see langword="false" /> otherwise.</returns>
495+
bool TryCreateLeast(
496+
IReadOnlyList<SqlExpression> expressions,
497+
Type resultType,
498+
[NotNullWhen(true)] out SqlExpression? leastExpression);
499+
500+
/// <summary>
501+
/// Attempts to creates a new expression that returns the greatest value from a list of expressions, e.g. an invocation of the
502+
/// <c>GREATEST</c> SQL function.
503+
/// </summary>
504+
/// <param name="expressions">An entity type to project.</param>
505+
/// <param name="resultType">The result CLR type for the returned expression.</param>
506+
/// <param name="greatestExpression">The expression which computes the greatest value.</param>
507+
/// <returns><see langword="true" /> if the expression could be created, <see langword="false" /> otherwise.</returns>
508+
bool TryCreateGreatest(
509+
IReadOnlyList<SqlExpression> expressions,
510+
Type resultType,
511+
[NotNullWhen(true)] out SqlExpression? greatestExpression);
486512
}

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

+51-33
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
265265
if (translated == QueryCompilationContext.NotTranslatedExpression)
266266
{
267267
// Attempt to translate access into a primitive collection property (i.e. array column)
268-
269268
if (_sqlTranslator.TryTranslatePropertyAccess(methodCallExpression, out var translatedExpression, out var property)
270269
&& property is IProperty { IsPrimitiveCollection: true } regularProperty
271270
&& translatedExpression is SqlExpression sqlExpression)
@@ -570,35 +569,8 @@ protected override ShapedQueryExpression TranslateConcat(ShapedQueryExpression s
570569
}
571570

572571
// Pattern-match Contains over ValuesExpression, translating to simplified 'item IN (1, 2, 3)' with constant elements
573-
if (source.QueryExpression is SelectExpression
574-
{
575-
Tables:
576-
[
577-
ValuesExpression
578-
{
579-
RowValues: [{ Values.Count: 2 }, ..],
580-
ColumnNames: [ValuesOrderingColumnName, ValuesValueColumnName]
581-
} valuesExpression
582-
],
583-
Predicate: null,
584-
GroupBy: [],
585-
Having: null,
586-
IsDistinct: false,
587-
Limit: null,
588-
Offset: null,
589-
// Note that in the context of Contains we don't care about orderings
590-
}
591-
// Make sure that the source projects the column from the ValuesExpression directly, i.e. no projection out with some expression
592-
&& projection is ColumnExpression projectedColumn
593-
&& projectedColumn.Table == valuesExpression)
572+
if (TryExtractBareInlineCollectionValues(source, out var values))
594573
{
595-
var values = new SqlExpression[valuesExpression.RowValues.Count];
596-
for (var i = 0; i < values.Length; i++)
597-
{
598-
// Skip the first value (_ord), which is irrelevant for Contains
599-
values[i] = valuesExpression.RowValues[i].Values[1];
600-
}
601-
602574
var inExpression = _sqlExpressionFactory.In(translatedItem, values);
603575
return source.Update(_sqlExpressionFactory.Select(inExpression), source.ShaperExpression);
604576
}
@@ -970,13 +942,19 @@ private SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerK
970942

971943
/// <inheritdoc />
972944
protected override ShapedQueryExpression? TranslateMax(ShapedQueryExpression source, LambdaExpression? selector, Type resultType)
973-
=> TranslateAggregateWithSelector(
974-
source, selector, t => QueryableMethods.MaxWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);
945+
=> TryExtractBareInlineCollectionValues(source, out var values)
946+
&& _sqlExpressionFactory.TryCreateGreatest(values, resultType, out var greatestExpression)
947+
? source.Update(_sqlExpressionFactory.Select(greatestExpression), source.ShaperExpression)
948+
: TranslateAggregateWithSelector(
949+
source, selector, t => QueryableMethods.MaxWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);
975950

976951
/// <inheritdoc />
977952
protected override ShapedQueryExpression? TranslateMin(ShapedQueryExpression source, LambdaExpression? selector, Type resultType)
978-
=> TranslateAggregateWithSelector(
979-
source, selector, t => QueryableMethods.MinWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);
953+
=> TryExtractBareInlineCollectionValues(source, out var values)
954+
&& _sqlExpressionFactory.TryCreateLeast(values, resultType, out var leastExpression)
955+
? source.Update(_sqlExpressionFactory.Select(leastExpression), source.ShaperExpression)
956+
: TranslateAggregateWithSelector(
957+
source, selector, t => QueryableMethods.MinWithoutSelector.MakeGenericMethod(t), throwWhenEmpty: true, resultType);
980958

981959
/// <inheritdoc />
982960
protected override ShapedQueryExpression? TranslateOfType(ShapedQueryExpression source, Type resultType)
@@ -2612,6 +2590,46 @@ private bool TryGetProjection(ShapedQueryExpression shapedQueryExpression, [NotN
26122590
return false;
26132591
}
26142592

2593+
private bool TryExtractBareInlineCollectionValues(ShapedQueryExpression shapedQuery, [NotNullWhen(true)] out SqlExpression[]? values)
2594+
{
2595+
if (TryGetProjection(shapedQuery, out var projection)
2596+
&& shapedQuery.QueryExpression is SelectExpression
2597+
{
2598+
Tables:
2599+
[
2600+
ValuesExpression
2601+
{
2602+
RowValues: [{ Values.Count: 2 }, ..],
2603+
ColumnNames: [ValuesOrderingColumnName, ValuesValueColumnName]
2604+
} valuesExpression
2605+
],
2606+
Predicate: null,
2607+
GroupBy: [],
2608+
Having: null,
2609+
IsDistinct: false,
2610+
Limit: null,
2611+
Offset: null,
2612+
// Note that we assume ordering doesn't matter (Contains/Min/Max)
2613+
}
2614+
// Make sure that the source projects the column from the ValuesExpression directly, i.e. no projection out with some expression
2615+
&& projection is ColumnExpression projectedColumn
2616+
&& projectedColumn.Table == valuesExpression)
2617+
{
2618+
values = new SqlExpression[valuesExpression.RowValues.Count];
2619+
2620+
for (var i = 0; i < values.Length; i++)
2621+
{
2622+
// Skip the first value (_ord) - this function assumes ordering doesn't matter
2623+
values[i] = valuesExpression.RowValues[i].Values[1];
2624+
}
2625+
2626+
return true;
2627+
}
2628+
2629+
values = null;
2630+
return false;
2631+
}
2632+
26152633
/// <summary>
26162634
/// A visitor which scans an expression tree and attempts to find columns for which we were missing type mappings (projected out
26172635
/// of queryable constant/parameter), and those type mappings have been inferred.

src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs

+47
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ private static readonly MethodInfo StringEqualsWithStringComparison
5151
private static readonly MethodInfo StringEqualsWithStringComparisonStatic
5252
= typeof(string).GetRuntimeMethod(nameof(string.Equals), new[] { typeof(string), typeof(string), typeof(StringComparison) })!;
5353

54+
private static readonly MethodInfo LeastMethodInfo
55+
= typeof(RelationalDbFunctionsExtensions).GetMethod(nameof(RelationalDbFunctionsExtensions.Least))!;
56+
57+
private static readonly MethodInfo GreatestMethodInfo
58+
= typeof(RelationalDbFunctionsExtensions).GetMethod(nameof(RelationalDbFunctionsExtensions.Greatest))!;
59+
5460
private static readonly MethodInfo GetTypeMethodInfo = typeof(object).GetTypeInfo().GetDeclaredMethod(nameof(GetType))!;
5561

5662
private readonly QueryCompilationContext _queryCompilationContext;
@@ -932,6 +938,47 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
932938
return QueryCompilationContext.NotTranslatedExpression;
933939
}
934940
}
941+
// Translate EF.Functions.Greatest/Least.
942+
// These are here rather than in a MethodTranslator since the parameter is an array, and that's not supported in regular
943+
// translation.
944+
else if (method.DeclaringType == typeof(RelationalDbFunctionsExtensions)
945+
&& method.IsGenericMethod
946+
&& method.GetGenericMethodDefinition() is var genericMethodDefinition
947+
&& (genericMethodDefinition == LeastMethodInfo || genericMethodDefinition == GreatestMethodInfo)
948+
&& methodCallExpression.Arguments[1] is NewArrayExpression newArray)
949+
{
950+
var values = newArray.Expressions;
951+
var translatedValues = new SqlExpression[values.Count];
952+
953+
for (var i = 0; i < values.Count; i++)
954+
{
955+
var value = values[i];
956+
var visitedValue = Visit(value);
957+
958+
if (TranslationFailed(value, visitedValue, out var translatedValue))
959+
{
960+
return QueryCompilationContext.NotTranslatedExpression;
961+
}
962+
963+
translatedValues[i] = translatedValue!;
964+
}
965+
966+
var elementClrType = newArray.Type.GetElementType()!;
967+
968+
if (genericMethodDefinition == LeastMethodInfo
969+
&& _sqlExpressionFactory.TryCreateLeast(translatedValues, elementClrType, out var leastExpression))
970+
{
971+
return leastExpression;
972+
}
973+
974+
if (genericMethodDefinition == GreatestMethodInfo
975+
&& _sqlExpressionFactory.TryCreateGreatest(translatedValues, elementClrType, out var greatestExpression))
976+
{
977+
return greatestExpression;
978+
}
979+
980+
throw new UnreachableException();
981+
}
935982
else
936983
{
937984
if (method.IsStatic

src/EFCore.Relational/Query/SqlExpressionFactory.cs

+24
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,30 @@ public virtual SelectExpression Select(IEntityType entityType, TableExpressionBa
675675
return selectExpression;
676676
}
677677

678+
/// <inheritdoc />
679+
public virtual bool TryCreateLeast(
680+
IReadOnlyList<SqlExpression> expressions,
681+
Type resultType,
682+
[NotNullWhen(true)] out SqlExpression? leastExpression)
683+
{
684+
var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions);
685+
leastExpression = Function(
686+
"LEAST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping);
687+
return true;
688+
}
689+
690+
/// <inheritdoc />
691+
public virtual bool TryCreateGreatest(
692+
IReadOnlyList<SqlExpression> expressions,
693+
Type resultType,
694+
[NotNullWhen(true)] out SqlExpression? greatestExpression)
695+
{
696+
var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions);
697+
greatestExpression = Function(
698+
"GREATEST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping);
699+
return true;
700+
}
701+
678702
/***
679703
* We need to add additional conditions on basic SelectExpression for certain cases
680704
* - If we are selecting from TPH then we need to add condition for discriminator if mapping is incomplete

src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.SqlServer/Properties/SqlServerStrings.resx

+3
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@
165165
<data name="DuplicateKeyMismatchedClustering" xml:space="preserve">
166166
<value>The keys {key1} on '{entityType1}' and {key2} on '{entityType2}' are both mapped to '{table}.{keyName}', but have different clustering configurations.</value>
167167
</data>
168+
<data name="LeastGreatestCompatibilityLevelTooLow" xml:space="preserve">
169+
<value>This usage of Math.Min or Math.Max requires SQL Server functions LEAST and GREATEST, which require compatibility level 160.</value>
170+
</data>
168171
<data name="IdentityBadType" xml:space="preserve">
169172
<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>
170173
</data>

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

+27
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
5+
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
6+
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
57
using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;
68

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

191+
if (method.DeclaringType == typeof(Math))
192+
{
193+
if (method.Name == nameof(Math.Min))
194+
{
195+
if (_sqlExpressionFactory.TryCreateLeast(
196+
new[] { arguments[0], arguments[1] }, method.ReturnType, out var leastExpression))
197+
{
198+
return leastExpression;
199+
}
200+
201+
throw new InvalidOperationException(SqlServerStrings.LeastGreatestCompatibilityLevelTooLow);
202+
}
203+
204+
if (method.Name == nameof(Math.Max))
205+
{
206+
if (_sqlExpressionFactory.TryCreateGreatest(
207+
new[] { arguments[0], arguments[1] }, method.ReturnType, out var leastExpression))
208+
{
209+
return leastExpression;
210+
}
211+
212+
throw new InvalidOperationException(SqlServerStrings.LeastGreatestCompatibilityLevelTooLow);
213+
}
214+
}
215+
189216
return null;
190217
}
191218
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal;
5+
46
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
57

68
/// <summary>

0 commit comments

Comments
 (0)