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 3239263b9a6..f2ed8297c2f 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) @@ -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); } @@ -960,13 +936,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) @@ -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; + } + /// /// 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..d81479b5577 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,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 diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 0c5ceea1124..1a408f27e20 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -675,6 +675,67 @@ 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); + + expressions = FlattenLeastGreatest("LEAST", 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); + + expressions = FlattenLeastGreatest("GREATEST", expressions); + + greatestExpression = Function( + "GREATEST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } + + private IReadOnlyList FlattenLeastGreatest(string functionName, IReadOnlyList expressions) + { + List? 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(); + 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 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/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/SqliteMathTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMathTranslator.cs index 087f207714e..fcd838ccef7 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteMathTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteMathTranslator.cs @@ -37,24 +37,6 @@ public class SqliteMathTranslator : IMethodCallTranslator { typeof(Math).GetMethod(nameof(Math.Log), new[] { typeof(double) })!, "ln" }, { typeof(Math).GetMethod(nameof(Math.Log2), new[] { typeof(double) })!, "log2" }, { typeof(Math).GetMethod(nameof(Math.Log10), new[] { typeof(double) })!, "log10" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(byte), typeof(byte) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(double), typeof(double) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(float), typeof(float) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(int), typeof(int) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(long), typeof(long) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(sbyte), typeof(sbyte) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(short), typeof(short) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(uint), typeof(uint) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Max), new[] { typeof(ushort), typeof(ushort) })!, "max" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(byte), typeof(byte) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(double), typeof(double) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(float), typeof(float) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(int), typeof(int) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(long), typeof(long) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(sbyte), typeof(sbyte) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(short), typeof(short) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(uint), typeof(uint) })!, "min" }, - { typeof(Math).GetMethod(nameof(Math.Min), new[] { typeof(ushort), typeof(ushort) })!, "min" }, { typeof(Math).GetMethod(nameof(Math.Pow), new[] { typeof(double), typeof(double) })!, "pow" }, { typeof(Math).GetMethod(nameof(Math.Round), new[] { typeof(double) })!, "round" }, { typeof(Math).GetMethod(nameof(Math.Sign), new[] { typeof(double) })!, "sign" }, @@ -179,6 +161,25 @@ public SqliteMathTranslator(ISqlExpressionFactory sqlExpressionFactory) typeMapping); } + if (method.DeclaringType == typeof(Math)) + { + if (method.Name == nameof(Math.Min)) + { + var success = _sqlExpressionFactory.TryCreateLeast( + new[] { arguments[0], arguments[1] }, method.ReturnType, out var leastExpression); + Check.DebugAssert(success, "Couldn't generate min"); + return leastExpression; + } + + if (method.Name == nameof(Math.Max)) + { + var success = _sqlExpressionFactory.TryCreateGreatest( + new[] { arguments[0], arguments[1] }, method.ReturnType, out var leastExpression); + Check.DebugAssert(success, "Couldn't generate max"); + return leastExpression; + } + } + return null; } } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs index 5ec5be61252..194dda9edcd 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlExpressionFactory.cs @@ -184,4 +184,65 @@ 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); + + expressions = FlattenLeastGreatest("min", 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); + + expressions = FlattenLeastGreatest("max", expressions); + + greatestExpression = Function( + "max", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } + + private IReadOnlyList FlattenLeastGreatest(string functionName, IReadOnlyList expressions) + { + List? 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(); + 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; + } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs index d9c72674697..e393283a8fa 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs @@ -593,6 +593,22 @@ public override async Task Where_math_min(bool async) AssertSql(); } + public override async Task Where_math_min_nested(bool async) + { + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_math_min(async)); + + AssertSql(); + } + + public override async Task Where_math_min_nested_twice(bool async) + { + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_math_min(async)); + + AssertSql(); + } + public override async Task Where_math_max(bool async) { // Cosmos client evaluation. Issue #17246. @@ -601,6 +617,22 @@ public override async Task Where_math_max(bool async) AssertSql(); } + public override async Task Where_math_max_nested(bool async) + { + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_math_max(async)); + + AssertSql(); + } + + public override async Task Where_math_max_nested_twice(bool async) + { + // Cosmos client evaluation. Issue #17246. + await AssertTranslationFailed(() => base.Where_math_max(async)); + + AssertSql(); + } + public override async Task Where_math_degrees(bool async) { // Cosmos client evaluation. Issue #17246. 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/NorthwindFunctionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs index e3cb71dbbac..e8d13e89834 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs @@ -1025,6 +1025,22 @@ public virtual Task Where_math_max(bool async) async, ss => ss.Set().Where(od => od.OrderID == 11077).Where(od => Math.Max(od.OrderID, od.ProductID) == od.OrderID)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_math_max_nested(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(od => od.OrderID == 11077) + .Where(od => Math.Max(od.OrderID, Math.Max(od.ProductID, 1)) == od.OrderID)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_math_max_nested_twice(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(od => od.OrderID == 11077) + .Where(od => Math.Max(Math.Max(1, Math.Max(od.OrderID, 2)), od.ProductID) == od.OrderID)); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_math_min(bool async) @@ -1033,6 +1049,22 @@ public virtual Task Where_math_min(bool async) ss => ss.Set().Where(od => od.OrderID == 11077) .Where(od => Math.Min(od.OrderID, od.ProductID) == od.ProductID)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_math_min_nested(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(od => od.OrderID == 11077) + .Where(od => Math.Min(od.OrderID, Math.Min(od.ProductID, 99999)) == od.ProductID)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Where_math_min_nested_twice(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(od => od.OrderID == 11077) + .Where(od => Math.Min(Math.Min(99999, Math.Min(od.OrderID, 99998)), od.ProductID) == od.ProductID)); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_math_degrees(bool async) diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 9e508b0a021..bd7a745dca5 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -154,6 +154,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/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index de790c5a869..98e2def94f3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -3061,13 +3061,14 @@ FROM [InheritanceOne] AS [i3] """); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task Nav_rewrite_doesnt_apply_null_protection_for_function_arguments(bool async) { await base.Nav_rewrite_doesnt_apply_null_protection_for_function_arguments(async); AssertSql( """ -SELECT [l0].[Level1_Required_Id] +SELECT GREATEST([l0].[Level1_Required_Id], 7) FROM [LevelOne] AS [l] LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[OneToOne_Optional_PK_Inverse2Id] WHERE [l0].[Id] IS NOT NULL diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 34189f763e2..adc3e377938 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -6461,13 +6461,14 @@ WHERE [t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] I """); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] public override async Task Nav_rewrite_doesnt_apply_null_protection_for_function_arguments(bool async) { await base.Nav_rewrite_doesnt_apply_null_protection_for_function_arguments(async); AssertSql( """ -SELECT [t].[Level1_Required_Id] +SELECT GREATEST([t].[Level1_Required_Id], 7) FROM [Level1] AS [l] LEFT JOIN ( SELECT [l0].[OneToOne_Required_PK_Date], [l0].[Level1_Required_Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs index 2b81d709c70..d7cee418c9e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs @@ -119,6 +119,46 @@ FROM [Customers] AS [c] """); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + 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 +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + 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..a0bb869ec3d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -1333,20 +1333,82 @@ FROM [Order Details] AS [o] """); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] 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] +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + public override async Task Where_math_min_nested(bool async) + { + await base.Where_math_min_nested(async); + + 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], 99999) = [o].[ProductID] +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + public override async Task Where_math_min_nested_twice(bool async) + { + await base.Where_math_min_nested_twice(async); + + AssertSql( + """ +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE [o].[OrderID] = 11077 AND LEAST(99999, [o].[OrderID], 99998, [o].[ProductID]) = [o].[ProductID] +"""); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] 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] +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + public override async Task Where_math_max_nested(bool async) + { + await base.Where_math_max_nested(async); + + 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], 1) = [o].[OrderID] +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + public override async Task Where_math_max_nested_twice(bool async) + { + await base.Where_math_max_nested_twice(async); + + AssertSql( + """ +SELECT [o].[OrderID], [o].[ProductID], [o].[Discount], [o].[Quantity], [o].[UnitPrice] +FROM [Order Details] AS [o] +WHERE [o].[OrderID] = 11077 AND GREATEST(1, [o].[OrderID], 2, [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 c296115aa63..e9b66808514 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -238,6 +238,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 bc2f95711bc..8bd0373a694 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -230,6 +230,62 @@ WHERE [p].[Id] NOT IN (2, 999) """); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + 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 +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + 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 +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + 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 +"""); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2022)] + 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.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs index 50bec38c0eb..031ff85e9d6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs @@ -16,8 +16,9 @@ public enum SqlServerCondition SupportsOnlineIndexes = 1 << 7, SupportsTemporalTablesCascadeDelete = 1 << 8, SupportsUtf8 = 1 << 9, - SupportsFunctions2019 = 1 << 10, - SupportsFunctions2017 = 1 << 11, - SupportsJsonPathExpressions = 1 << 12, - SupportsSqlClr = 1 << 13, + SupportsJsonPathExpressions = 1 << 10, + SupportsSqlClr = 1 << 11, + SupportsFunctions2017 = 1 << 12, + SupportsFunctions2019 = 1 << 13, + SupportsFunctions2022 = 1 << 14, } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs index 0bd311c2357..356279b6019 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs @@ -72,9 +72,14 @@ public ValueTask IsMetAsync() isMet &= TestEnvironment.IsUtf8Supported; } - if (Conditions.HasFlag(SqlServerCondition.SupportsFunctions2019)) + if (Conditions.HasFlag(SqlServerCondition.SupportsJsonPathExpressions)) { - isMet &= TestEnvironment.IsFunctions2019Supported; + isMet &= TestEnvironment.SupportsJsonPathExpressions; + } + + if (Conditions.HasFlag(SqlServerCondition.SupportsSqlClr)) + { + isMet &= TestEnvironment.IsSqlClrSupported; } if (Conditions.HasFlag(SqlServerCondition.SupportsFunctions2017)) @@ -82,14 +87,14 @@ public ValueTask IsMetAsync() isMet &= TestEnvironment.IsFunctions2017Supported; } - if (Conditions.HasFlag(SqlServerCondition.SupportsJsonPathExpressions)) + if (Conditions.HasFlag(SqlServerCondition.SupportsFunctions2019)) { - isMet &= TestEnvironment.SupportsJsonPathExpressions; + isMet &= TestEnvironment.IsFunctions2019Supported; } - if (Conditions.HasFlag(SqlServerCondition.SupportsSqlClr)) + if (Conditions.HasFlag(SqlServerCondition.SupportsFunctions2022)) { - isMet &= TestEnvironment.IsSqlClrSupported; + isMet &= TestEnvironment.IsFunctions2022Supported; } return ValueTask.FromResult(isMet); diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs index 2cd91569932..67c5831d7c6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs @@ -41,11 +41,13 @@ public static class TestEnvironment private static bool? _supportsUtf8; + private static bool? _supportsJsonPathExpressions; + private static bool? _supportsFunctions2017; private static bool? _supportsFunctions2019; - private static bool? _supportsJsonPathExpressions; + private static bool? _supportsFunctions2022; private static byte? _productMajorVersion; @@ -288,6 +290,33 @@ public static bool IsUtf8Supported } } + public static bool SupportsJsonPathExpressions + { + get + { + if (!IsConfigured) + { + return false; + } + + if (_supportsJsonPathExpressions.HasValue) + { + return _supportsJsonPathExpressions.Value; + } + + try + { + _supportsJsonPathExpressions = GetProductMajorVersion() >= 14 || IsSqlAzure; + } + catch (PlatformNotSupportedException) + { + _supportsJsonPathExpressions = false; + } + + return _supportsJsonPathExpressions.Value; + } + } + public static bool IsFunctions2017Supported { get @@ -342,7 +371,7 @@ public static bool IsFunctions2019Supported } } - public static bool SupportsJsonPathExpressions + public static bool IsFunctions2022Supported { get { @@ -351,21 +380,21 @@ public static bool SupportsJsonPathExpressions return false; } - if (_supportsJsonPathExpressions.HasValue) + if (_supportsFunctions2022.HasValue) { - return _supportsJsonPathExpressions.Value; + return _supportsFunctions2022.Value; } try { - _supportsJsonPathExpressions = GetProductMajorVersion() >= 14 || IsSqlAzure; + _supportsFunctions2022 = GetProductMajorVersion() >= 16 || IsSqlAzure; } catch (PlatformNotSupportedException) { - _supportsJsonPathExpressions = false; + _supportsFunctions2022 = false; } - return _supportsJsonPathExpressions.Value; + return _supportsFunctions2022.Value; } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index 530817b447e..0074fb1b536 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -1196,6 +1196,30 @@ public override async Task Where_math_min(bool async) """); } + public override async Task Where_math_min_nested(bool async) + { + await base.Where_math_min_nested(async); + + AssertSql( + """ +SELECT "o"."OrderID", "o"."ProductID", "o"."Discount", "o"."Quantity", "o"."UnitPrice" +FROM "Order Details" AS "o" +WHERE "o"."OrderID" = 11077 AND min("o"."OrderID", "o"."ProductID", 99999) = "o"."ProductID" +"""); + } + + public override async Task Where_math_min_nested_twice(bool async) + { + await base.Where_math_min_nested_twice(async); + + AssertSql( + """ +SELECT "o"."OrderID", "o"."ProductID", "o"."Discount", "o"."Quantity", "o"."UnitPrice" +FROM "Order Details" AS "o" +WHERE "o"."OrderID" = 11077 AND min(99999, "o"."OrderID", 99998, "o"."ProductID") = "o"."ProductID" +"""); + } + public override async Task Where_math_max(bool async) { await base.Where_math_max(async); @@ -1208,6 +1232,30 @@ public override async Task Where_math_max(bool async) """); } + public override async Task Where_math_max_nested(bool async) + { + await base.Where_math_max_nested(async); + + AssertSql( + """ +SELECT "o"."OrderID", "o"."ProductID", "o"."Discount", "o"."Quantity", "o"."UnitPrice" +FROM "Order Details" AS "o" +WHERE "o"."OrderID" = 11077 AND max("o"."OrderID", "o"."ProductID", 1) = "o"."OrderID" +"""); + } + + public override async Task Where_math_max_nested_twice(bool async) + { + await base.Where_math_max_nested_twice(async); + + AssertSql( + """ +SELECT "o"."OrderID", "o"."ProductID", "o"."Discount", "o"."Quantity", "o"."UnitPrice" +FROM "Order Details" AS "o" +WHERE "o"."OrderID" = 11077 AND max(1, "o"."OrderID", 2, "o"."ProductID") = "o"."OrderID" +"""); + } + public override async Task Where_string_to_lower(bool async) { await base.Where_string_to_lower(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index b2cec826dcd..86730055fb7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -231,6 +231,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);