diff --git a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs index f3a3f86be73..f0f6a0e1ea8 100644 --- a/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs @@ -36,4 +36,24 @@ public static TProperty Collate( TProperty operand, [NotParameterized] string collation) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Collate))); + + /// + /// Returns the smallest value from the given list of values. Usually corresponds to the LEAST SQL function. + /// + /// The instance. + /// The list of values from which return the smallest value. + public static T Least( + this DbFunctions _, + [NotParameterized] params T[] values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Least))); + + /// + /// Returns the greatest value from the given list of values. Usually corresponds to the GREATEST SQL function. + /// + /// The instance. + /// The list of values from which return the greatest value. + public static T Greatest( + this DbFunctions _, + [NotParameterized] params T[] values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(Greatest))); } diff --git a/src/EFCore.Relational/Query/ExpressionExtensions.cs b/src/EFCore.Relational/Query/ExpressionExtensions.cs index ed931e80f1c..f56c73011cd 100644 --- a/src/EFCore.Relational/Query/ExpressionExtensions.cs +++ b/src/EFCore.Relational/Query/ExpressionExtensions.cs @@ -34,4 +34,23 @@ public static class ExpressionExtensions return null; } + + /// + /// Infers type mapping from given s. + /// + /// Expressions to search for to find the type mapping. + /// A relational type mapping inferred from the expressions. + public static RelationalTypeMapping? InferTypeMapping(IReadOnlyList expressions) + { + for (var i = 0; i < expressions.Count; i++) + { + var sql = expressions[i]; + if (sql.TypeMapping != null) + { + return sql.TypeMapping; + } + } + + return null; + } } diff --git a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs index 6462d240fb6..b435a252732 100644 --- a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs @@ -483,4 +483,30 @@ SqlFunctionExpression NiladicFunction( /// A table source to project from. /// An expression representing a SELECT in a SQL tree. SelectExpression Select(IEntityType entityType, TableExpressionBase tableExpressionBase); + + /// + /// Attempts to creates a new expression that returns the smallest value from a list of expressions, e.g. an invocation of the + /// LEAST SQL function. + /// + /// An entity type to project. + /// The result CLR type for the returned expression. + /// The expression which computes the smallest value. + /// if the expression could be created, otherwise. + bool TryCreateLeast( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? leastExpression); + + /// + /// Attempts to creates a new expression that returns the greatest value from a list of expressions, e.g. an invocation of the + /// GREATEST SQL function. + /// + /// An entity type to project. + /// The result CLR type for the returned expression. + /// The expression which computes the greatest value. + /// if the expression could be created, otherwise. + bool TryCreateGreatest( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? greatestExpression); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index f4defb8a3fb..fe2a36a5baa 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -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) @@ -570,35 +569,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 - { - RowValues: [{ Values.Count: 2 }, ..], - 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); } @@ -970,13 +942,19 @@ private SqlExpression CreateJoinPredicate(Expression outerKey, Expression innerK /// 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); /// 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); /// protected override ShapedQueryExpression? TranslateOfType(ShapedQueryExpression source, Type resultType) @@ -2612,6 +2590,46 @@ 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 + { + RowValues: [{ Values.Count: 2 }, ..], + 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; + } + /// /// 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. diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 7850ba29b23..20bd725e6a7 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -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; @@ -932,6 +938,49 @@ 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()!; + + // TODO: Type mappings! + + 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 diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 0c5ceea1124..f7eb3b4fdee 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -675,6 +675,30 @@ public virtual SelectExpression Select(IEntityType entityType, TableExpressionBa return selectExpression; } + /// + public virtual bool TryCreateLeast( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? leastExpression) + { + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + leastExpression = Function( + "LEAST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } + + /// + public virtual bool TryCreateGreatest( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? greatestExpression) + { + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + greatestExpression = Function( + "GREATEST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } + /*** * 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 diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index a36445d1869..a290d7f8158 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -147,6 +147,12 @@ public static string DuplicateKeyMismatchedClustering(object? key1, object? enti GetString("DuplicateKeyMismatchedClustering", nameof(key1), nameof(entityType1), nameof(key2), nameof(entityType2), nameof(table), nameof(keyName)), key1, entityType1, key2, entityType2, table, keyName); + /// + /// This usage of Math.Min or Math.Max requires SQL Server functions LEAST and GREATEST, which require compatibility level 160. + /// + public static string LeastGreatestCompatibilityLevelTooLow + => GetString("LeastGreatestCompatibilityLevelTooLow"); + /// /// 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. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 59c92b4e2b0..e2c0e595522 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -165,6 +165,9 @@ The keys {key1} on '{entityType1}' and {key2} on '{entityType2}' are both mapped to '{table}.{keyName}', but have different clustering configurations. + + This usage of Math.Min or Math.Max requires SQL Server functions LEAST and GREATEST, which require compatibility level 160. + 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. diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMathTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMathTranslator.cs index e1ab646fb21..7b455342195 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMathTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMathTranslator.cs @@ -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; @@ -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; } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs index 6abb498a288..a9e4b654e89 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs @@ -1,6 +1,8 @@ // 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.SqlServer.Infrastructure.Internal; + namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs index 20201580b02..da4f7fa9856 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; @@ -15,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; public class SqlServerSqlExpressionFactory : SqlExpressionFactory { private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly int _sqlServerCompatibilityLevel; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -22,10 +24,13 @@ public class SqlServerSqlExpressionFactory : SqlExpressionFactory /// 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. /// - public SqlServerSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) + public SqlServerSqlExpressionFactory( + SqlExpressionFactoryDependencies dependencies, + ISqlServerSingletonOptions sqlServerSingletonOptions) : base(dependencies) { _typeMappingSource = dependencies.TypeMappingSource; + _sqlServerCompatibilityLevel = sqlServerSingletonOptions.CompatibilityLevel; } /// @@ -62,4 +67,34 @@ private SqlExpression ApplyTypeMappingOnAtTimeZone(AtTimeZoneExpression atTimeZo atTimeZoneExpression.Type, typeMapping); } + + /// + public override bool TryCreateLeast( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? leastExpression) + { + if (_sqlServerCompatibilityLevel >= 160) + { + return base.TryCreateLeast(expressions, resultType, out leastExpression); + } + + leastExpression = null; + return false; + } + + /// + public override bool TryCreateGreatest( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? greatestExpression) + { + if (_sqlServerCompatibilityLevel >= 160) + { + return base.TryCreateGreatest(expressions, resultType, out greatestExpression); + } + + greatestExpression = null; + return false; + } } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs index 5ec5be61252..9766cd55283 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs @@ -184,4 +184,28 @@ private SqlExpression ApplyTypeMappingOnGlob(GlobExpression globExpression) ? new RegexpExpression(match, pattern, _boolTypeMapping) : regexpExpression; } + + /// + public override bool TryCreateLeast( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? leastExpression) + { + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + leastExpression = Function( + "MIN", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } + + /// + public override bool TryCreateGreatest( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? greatestExpression) + { + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + greatestExpression = Function( + "MAX", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.cs index 0666285093e..0c55bc50430 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindFunctionsQueryRelationalTestBase.cs @@ -1,6 +1,8 @@ // 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.TestModels.Northwind; + namespace Microsoft.EntityFrameworkCore.Query; public abstract class NorthwindFunctionsQueryRelationalTestBase : NorthwindFunctionsQueryTestBase diff --git a/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs index 87fa9b6f736..db705b79cda 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/RelationalNorthwindDbFunctionsQueryTestBase.cs @@ -50,6 +50,48 @@ public virtual Task Collate_case_sensitive_constant(bool async) c => c.ContactName == EF.Functions.Collate("maria anders", CaseSensitiveCollation), c => c.ContactName.Equals("maria anders", StringComparison.Ordinal)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Least(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(od => EF.Functions.Least(od.OrderID, 10251) == 10251), + ss => ss.Set().Where(od => Math.Min(od.OrderID, 10251) == 10251)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Greatest(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(od => EF.Functions.Greatest(od.OrderID, 10251) == 10251), + ss => ss.Set().Where(od => Math.Max(od.OrderID, 10251) == 10251)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Least_with_parameter_array_is_not_supported(bool async) + { + var arr = new[] { 1, 2 }; + + await AssertTranslationFailed( + () => + AssertQuery( + async, + ss => ss.Set().Where(od => EF.Functions.Least(arr) == 10251))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Greatest_with_parameter_array_is_not_supported(bool async) + { + var arr = new[] { 1, 2 }; + + await AssertTranslationFailed( + () => + AssertQuery( + async, + ss => ss.Set().Where(od => EF.Functions.Greatest(arr) == 10251))); + } + protected abstract string CaseInsensitiveCollation { get; } protected abstract string CaseSensitiveCollation { get; } } diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 7891d0ae0b2..41f3a76d8bd 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -141,6 +141,42 @@ public virtual Task Inline_collection_negated_Contains_as_All(bool async) async, ss => ss.Set().Where(c => new[] { 2, 999 }.All(i => i != c.Id))); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Inline_collection_Min_with_two_values(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 30, c.Int }.Min() == 30)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Inline_collection_Max_with_two_values(bool async) + => await AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 30, c.Int }.Max() == 30)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Inline_collection_Min_with_three_values(bool async) + { + var i = 25; + + await AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 30, c.Int, i }.Min() == 25)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Inline_collection_Max_with_three_values(bool async) + { + var i = 35; + + await AssertQuery( + async, + ss => ss.Set().Where(c => new[] { 30, c.Int, i }.Max() == 35)); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Parameter_collection_Count(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs index 2b81d709c70..877db0a832c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs @@ -119,6 +119,44 @@ FROM [Customers] AS [c] """); } + public override async Task Least(bool async) + { + await base.Least(async); + + AssertSql( + """ +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE LEAST([o].[OrderID], 10251) = 10251 +"""); + } + + public override async Task Greatest(bool async) + { + await base.Greatest(async); + + AssertSql( + """ +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE GREATEST([o].[OrderID], 10251) = 10251 +"""); + } + + public override async Task Least_with_parameter_array_is_not_supported(bool async) + { + await base.Least_with_parameter_array_is_not_supported(async); + + AssertSql(); + } + + public override async Task Greatest_with_parameter_array_is_not_supported(bool async) + { + await base.Greatest_with_parameter_array_is_not_supported(async); + + AssertSql(); + } + protected override string CaseInsensitiveCollation => "Latin1_General_CI_AI"; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 9ac60861053..9f9264e10c0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -1335,18 +1335,26 @@ FROM [Order Details] AS [o] public override async Task Where_math_min(bool async) { - // Translate Math.Min. - await AssertTranslationFailed(() => base.Where_math_min(async)); + await base.Where_math_min(async); - AssertSql(); + AssertSql( + """ +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE [o].[OrderID] = 11077 AND LEAST([o].[OrderID], [o].[ProductID]) = [o].[ProductID] +"""); } public override async Task Where_math_max(bool async) { - // Translate Math.Max. - await AssertTranslationFailed(() => base.Where_math_max(async)); + await base.Where_math_max(async); - AssertSql(); + AssertSql( + """ +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE [o].[OrderID] = 11077 AND GREATEST([o].[OrderID], [o].[ProductID]) = [o].[OrderID] +"""); } public override async Task Where_math_degrees(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 9a78d37f910..d8238c253cf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -217,6 +217,66 @@ WHERE [p].[Id] NOT IN (2, 999) """); } + public override async Task Inline_collection_Min_with_two_values(bool async) + { + await base.Inline_collection_Min_with_two_values(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT MIN([v].[Value]) + FROM (VALUES (CAST(30 AS int)), ([p].[Int])) AS [v]([Value])) = 30 +"""); + } + + public override async Task Inline_collection_Max_with_two_values(bool async) + { + await base.Inline_collection_Max_with_two_values(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT MAX([v].[Value]) + FROM (VALUES (CAST(30 AS int)), ([p].[Int])) AS [v]([Value])) = 30 +"""); + } + + public override async Task Inline_collection_Min_with_three_values(bool async) + { + await base.Inline_collection_Min_with_three_values(async); + + AssertSql( + """ +@__i_0='25' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT MIN([v].[Value]) + FROM (VALUES (CAST(30 AS int)), ([p].[Int]), (@__i_0)) AS [v]([Value])) = 25 +"""); + } + + public override async Task Inline_collection_Max_with_three_values(bool async) + { + await base.Inline_collection_Max_with_three_values(async); + + AssertSql( + """ +@__i_0='35' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE ( + SELECT MAX([v].[Value]) + FROM (VALUES (CAST(30 AS int)), ([p].[Int]), (@__i_0)) AS [v]([Value])) = 35 +"""); + } + public override Task Parameter_collection_Count(bool async) => AssertCompatibilityLevelTooLow(() => base.Parameter_collection_Count(async)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index b7016e96076..8256479bdc7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -209,6 +209,58 @@ WHERE [p].[Id] NOT IN (2, 999) """); } + public override async Task Inline_collection_Min_with_two_values(bool async) + { + await base.Inline_collection_Min_with_two_values(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE LEAST(30, [p].[Int]) = 30 +"""); + } + + public override async Task Inline_collection_Max_with_two_values(bool async) + { + await base.Inline_collection_Max_with_two_values(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE GREATEST(30, [p].[Int]) = 30 +"""); + } + + public override async Task Inline_collection_Min_with_three_values(bool async) + { + await base.Inline_collection_Min_with_three_values(async); + + AssertSql( + """ +@__i_0='25' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE LEAST(30, [p].[Int], @__i_0) = 25 +"""); + } + + public override async Task Inline_collection_Max_with_three_values(bool async) + { + await base.Inline_collection_Max_with_three_values(async); + + AssertSql( + """ +@__i_0='35' + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE GREATEST(30, [p].[Int], @__i_0) = 35 +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 83d46064923..9c9687ed215 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -210,6 +210,58 @@ public override async Task Inline_collection_negated_Contains_as_All(bool async) """); } + public override async Task Inline_collection_Min_with_two_values(bool async) + { + await base.Inline_collection_Min_with_two_values(async); + + AssertSql( + """ +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE MIN(30, "p"."Int") = 30 +"""); + } + + public override async Task Inline_collection_Max_with_two_values(bool async) + { + await base.Inline_collection_Max_with_two_values(async); + + AssertSql( + """ +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE MAX(30, "p"."Int") = 30 +"""); + } + + public override async Task Inline_collection_Min_with_three_values(bool async) + { + await base.Inline_collection_Min_with_three_values(async); + + AssertSql( + """ +@__i_0='25' + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE MIN(30, "p"."Int", @__i_0) = 25 +"""); + } + + public override async Task Inline_collection_Max_with_three_values(bool async) + { + await base.Inline_collection_Max_with_three_values(async); + + AssertSql( + """ +@__i_0='35' + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE MAX(30, "p"."Int", @__i_0) = 35 +"""); + } + public override async Task Parameter_collection_Count(bool async) { await base.Parameter_collection_Count(async);