diff --git a/src/EFCore.Relational/Query/Internal/QueryableAggregateMethodTranslator.cs b/src/EFCore.Relational/Query/Internal/QueryableAggregateMethodTranslator.cs index 1d1e9223508..2aa27685733 100644 --- a/src/EFCore.Relational/Query/Internal/QueryableAggregateMethodTranslator.cs +++ b/src/EFCore.Relational/Query/Internal/QueryableAggregateMethodTranslator.cs @@ -74,6 +74,8 @@ public QueryableAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFac averageSqlExpression.Type, averageSqlExpression.TypeMapping); + // Count/LongCount are special since if the argument is a star fragment, it needs to be transformed to any non-null constant + // when a predicate is applied. case nameof(Queryable.Count) when methodInfo == QueryableMethods.CountWithoutPredicate || methodInfo == QueryableMethods.CountWithPredicate: diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs index cfd848e8eb8..0b4f646d447 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlFunctionExpression.cs @@ -250,7 +250,6 @@ private SqlFunctionExpression( /// /// A list of bool values indicating whether individual argument propagate null to the result. /// - public virtual IReadOnlyList? ArgumentsPropagateNullability { get; } /// diff --git a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs index f4de9db02ab..3a74c14b1d6 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs @@ -1078,7 +1078,7 @@ public static int DateDiffWeek( /// /// Validate if the given string is a valid date. - /// Corresponds to the SQL Server's ISDATE('date'). + /// Corresponds to SQL Server's ISDATE('date'). /// /// /// See Database functions, and @@ -1096,7 +1096,7 @@ public static bool IsDate( /// /// Initializes a new instance of the structure to the specified year, month, day, hour, minute, second, /// and millisecond. - /// Corresponds to the SQL Server's DATETIMEFROMPARTS(year, month, day, hour, minute, second, millisecond). + /// Corresponds to SQL Server's DATETIMEFROMPARTS(year, month, day, hour, minute, second, millisecond). /// /// /// See Database functions, and @@ -1128,7 +1128,7 @@ public static DateTime DateTimeFromParts( /// /// Initializes a new instance of the structure to the specified year, month, day. - /// Corresponds to the SQL Server's DATEFROMPARTS(year, month, day). + /// Corresponds to SQL Server's DATEFROMPARTS(year, month, day). /// /// /// See Database functions, and @@ -1150,7 +1150,7 @@ public static DateTime DateFromParts( /// /// Initializes a new instance of the structure to the specified year, month, day, hour, minute, second, /// fractions, and precision. - /// Corresponds to the SQL Server's DATETIME2FROMPARTS(year, month, day, hour, minute, seconds, fractions, precision). + /// Corresponds to SQL Server's DATETIME2FROMPARTS(year, month, day, hour, minute, seconds, fractions, precision). /// /// /// See Database functions, and @@ -1185,7 +1185,7 @@ public static DateTime DateTime2FromParts( /// /// Initializes a new instance of the structure to the specified year, month, day, hour, minute, /// second, fractions, hourOffset, minuteOffset and precision. - /// Corresponds to the SQL Server's + /// Corresponds to SQL Server's /// /// DATETIMEOFFSETFROMPARTS(year, month, day, hour, minute, seconds, fractions, hour_offset, /// minute_offset, precision) @@ -1228,7 +1228,7 @@ public static DateTimeOffset DateTimeOffsetFromParts( /// /// Initializes a new instance of the structure to the specified year, month, day, hour and minute. - /// Corresponds to the SQL Server's SMALLDATETIMEFROMPARTS(year, month, day, hour, minute). + /// Corresponds to SQL Server's SMALLDATETIMEFROMPARTS(year, month, day, hour, minute). /// /// /// See Database functions, and @@ -1253,7 +1253,7 @@ public static DateTime SmallDateTimeFromParts( /// /// Initializes a new instance of the structure to the specified hour, minute, second, fractions, and - /// precision. Corresponds to the SQL Server's TIMEFROMPARTS(hour, minute, seconds, fractions, precision). + /// precision. Corresponds to SQL Server's TIMEFROMPARTS(hour, minute, seconds, fractions, precision). /// /// /// See Database functions, and @@ -1424,7 +1424,7 @@ public static TimeSpan TimeFromParts( /// /// Validate if the given string is a valid numeric. - /// Corresponds to the SQL Server's ISNUMERIC(expression). + /// Corresponds to the SQL Server ISNUMERIC(expression). /// /// /// See Database functions, and @@ -1441,7 +1441,7 @@ public static bool IsNumeric( /// /// Converts to the corresponding datetimeoffset in the target . - /// Corresponds to the SQL Server's AT TIME ZONE construct. + /// Corresponds to the SQL Server AT TIME ZONE construct. /// /// /// @@ -1467,7 +1467,7 @@ public static DateTimeOffset AtTimeZone( /// /// Converts to the time zone specified by . - /// Corresponds to the SQL Server's AT TIME ZONE construct. + /// Corresponds to the SQL Server AT TIME ZONE construct. /// /// /// See Database functions, and @@ -1484,4 +1484,300 @@ public static DateTimeOffset AtTimeZone( DateTimeOffset dateTimeOffset, string timeZone) => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(AtTimeZone))); + + #region Sample standard deviation + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEV. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEV. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEV. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEV. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEV. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEV. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + /// + /// Returns the sample standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEV. + /// + /// The instance. + /// The values. + /// The computed sample standard deviation. + public static double? StandardDeviationSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationSample))); + + #endregion Sample standard deviation + + #region Population standard deviation + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEVP. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEVP. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEVP. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEVP. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEVP. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEVP. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + /// + /// Returns the population standard deviation of all values in the specified expression. + /// Corresponds to SQL Server's STDEVP. + /// + /// The instance. + /// The values. + /// The computed population standard deviation. + public static double? StandardDeviationPopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(StandardDeviationPopulation))); + + #endregion Population standard deviation + + #region Sample variance + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to SQL Server's VAR. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to SQL Server's VAR. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to SQL Server's VAR. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to SQL Server's VAR. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to SQL Server's VAR. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to SQL Server's VAR. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + /// + /// Returns the sample variance of all values in the specified expression. + /// Corresponds to SQL Server's VAR. + /// + /// The instance. + /// The values. + /// The computed sample variance. + public static double? VarianceSample(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VarianceSample))); + + #endregion Sample variance + + #region Population variance + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to SQL Server's VARP. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to SQL Server's VARP. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to SQL Server's VARP. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to SQL Server's VARP. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to SQL Server's VARP. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to SQL Server's VARP. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + /// + /// Returns the population variance of all values in the specified expression. + /// Corresponds to SQL Server's VARP. + /// + /// The instance. + /// The values. + /// The computed population variance. + public static double? VariancePopulation(this DbFunctions _, IEnumerable values) + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); + + #endregion Population variance } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateFunctionExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateFunctionExpression.cs new file mode 100644 index 00000000000..8d50ac501ab --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateFunctionExpression.cs @@ -0,0 +1,224 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerAggregateFunctionExpression : SqlExpression +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerAggregateFunctionExpression( + string name, + IReadOnlyList arguments, + IReadOnlyList orderings, + bool nullable, + IEnumerable argumentsPropagateNullability, + Type type, + RelationalTypeMapping? typeMapping) + : base(type, typeMapping) + { + Name = name; + Arguments = arguments.ToList(); + Orderings = orderings; + IsNullable = nullable; + ArgumentsPropagateNullability = argumentsPropagateNullability.ToList(); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string Name { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList Arguments { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList Orderings { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsNullable { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList ArgumentsPropagateNullability { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + SqlExpression[]? arguments = null; + for (var i = 0; i < Arguments.Count; i++) + { + var visitedArgument = (SqlExpression)visitor.Visit(Arguments[i]); + if (visitedArgument != Arguments[i] && arguments is null) + { + arguments = new SqlExpression[Arguments.Count]; + + for (var j = 0; j < i; j++) + { + arguments[j] = Arguments[j]; + } + } + + if (arguments is not null) + { + arguments[i] = visitedArgument; + } + } + + OrderingExpression[]? orderings = null; + for (var i = 0; i < Orderings.Count; i++) + { + var visitedOrdering = (OrderingExpression)visitor.Visit(Orderings[i]); + if (visitedOrdering != Orderings[i] && orderings is null) + { + orderings = new OrderingExpression[Orderings.Count]; + + for (var j = 0; j < i; j++) + { + orderings[j] = Orderings[j]; + } + } + + if (orderings is not null) + { + orderings[i] = visitedOrdering; + } + } + + return arguments is not null || orderings is not null + ? new SqlServerAggregateFunctionExpression( + Name, + arguments ?? Arguments, + orderings ?? Orderings, + IsNullable, + ArgumentsPropagateNullability, + Type, + TypeMapping) + : this; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlServerAggregateFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) + => new( + Name, + Arguments, + Orderings, + IsNullable, + ArgumentsPropagateNullability, + Type, + typeMapping ?? TypeMapping); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlServerAggregateFunctionExpression Update( + IReadOnlyList arguments, + IReadOnlyList orderings) + => (ReferenceEquals(arguments, Arguments) || arguments.SequenceEqual(Arguments)) + && (ReferenceEquals(orderings, Orderings) || orderings.SequenceEqual(Orderings)) + ? this + : new SqlServerAggregateFunctionExpression( + Name, + arguments, + orderings, + IsNullable, + ArgumentsPropagateNullability, + Type, + TypeMapping); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append(Name); + + expressionPrinter.Append("("); + expressionPrinter.VisitCollection(Arguments); + expressionPrinter.Append(")"); + + if (Orderings.Count > 0) + { + expressionPrinter.Append(" WITHIN GROUP (ORDER BY "); + expressionPrinter.VisitCollection(Orderings); + expressionPrinter.Append(")"); + } + } + + /// + public override bool Equals(object? obj) + => obj is SqlServerAggregateFunctionExpression sqlServerFunctionExpression && Equals(sqlServerFunctionExpression); + + private bool Equals(SqlServerAggregateFunctionExpression? other) + => ReferenceEquals(this, other) + || other is not null + && base.Equals(other) + && Name == other.Name + && Arguments.SequenceEqual(other.Arguments) + && Orderings.SequenceEqual(other.Orderings); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + hash.Add(Name); + + for (var i = 0; i < Arguments.Count; i++) + { + hash.Add(Arguments[i]); + } + + for (var i = 0; i < Orderings.Count; i++) + { + hash.Add(Orderings[i]); + } + + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateMethodCallTranslatorProvider.cs index ba39f384e0f..9973b859434 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerAggregateMethodCallTranslatorProvider.cs @@ -21,10 +21,14 @@ public SqlServerAggregateMethodCallTranslatorProvider(RelationalAggregateMethodC : base(dependencies) { var sqlExpressionFactory = dependencies.SqlExpressionFactory; + var typeMappingSource = dependencies.RelationalTypeMappingSource; + AddTranslators( new IAggregateMethodCallTranslator[] { - new SqlServerLongCountMethodTranslator(sqlExpressionFactory) + new SqlServerLongCountMethodTranslator(sqlExpressionFactory), + new SqlServerStatisticsAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource), + new SqlServerStringAggregateMethodTranslator(sqlExpressionFactory, typeMappingSource) }); } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerExpression.cs new file mode 100644 index 00000000000..c211ccbeab0 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerExpression.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public static class SqlServerExpression +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static SqlFunctionExpression AggregateFunction( + ISqlExpressionFactory sqlExpressionFactory, + string name, + IEnumerable arguments, + EnumerableExpression enumerableExpression, + int enumerableArgumentIndex, + bool nullable, + IEnumerable argumentsPropagateNullability, + Type returnType, + RelationalTypeMapping? typeMapping = null) + => new( + name, + ProcessAggregateFunctionArguments(sqlExpressionFactory, arguments, enumerableExpression, enumerableArgumentIndex), + nullable, + argumentsPropagateNullability, + returnType, + typeMapping); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static SqlExpression AggregateFunctionWithOrdering( + ISqlExpressionFactory sqlExpressionFactory, + string name, + IEnumerable arguments, + EnumerableExpression enumerableExpression, + int enumerableArgumentIndex, + bool nullable, + IEnumerable argumentsPropagateNullability, + Type returnType, + RelationalTypeMapping? typeMapping = null) + => enumerableExpression.Orderings.Count == 0 + ? AggregateFunction(sqlExpressionFactory, name, arguments, enumerableExpression, enumerableArgumentIndex, nullable, argumentsPropagateNullability, returnType, typeMapping) + : new SqlServerAggregateFunctionExpression( + name, + ProcessAggregateFunctionArguments(sqlExpressionFactory, arguments, enumerableExpression, enumerableArgumentIndex), + enumerableExpression.Orderings, + nullable, + argumentsPropagateNullability, + returnType, + typeMapping); + + private static IReadOnlyList ProcessAggregateFunctionArguments( + ISqlExpressionFactory sqlExpressionFactory, + IEnumerable arguments, + EnumerableExpression enumerableExpression, + int enumerableArgumentIndex) + { + var argIndex = 0; + var typeMappedArguments = new List(); + + foreach (var argument in arguments) + { + var modifiedArgument = sqlExpressionFactory.ApplyDefaultTypeMapping(argument); + + if (argIndex == enumerableArgumentIndex) + { + // This is the argument representing the enumerable inputs to be aggregated. + // Wrap it with a CASE/WHEN for the predicate and with DISTINCT, if necessary. + if (enumerableExpression.Predicate != null) + { + modifiedArgument = sqlExpressionFactory.Case( + new List { new(enumerableExpression.Predicate, modifiedArgument) }, + elseResult: null); + } + + if (enumerableExpression.IsDistinct) + { + modifiedArgument = new DistinctExpression(modifiedArgument); + } + } + + typeMappedArguments.Add(modifiedArgument); + + argIndex++; + } + + return typeMappedArguments; + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs index 01a3d27c7c8..5194c71233b 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs @@ -46,4 +46,14 @@ public override Expression Optimize( return new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory).Visit(optimizedQueryExpression); } + + /// + protected override Expression ProcessSqlNullability( + Expression selectExpression, IReadOnlyDictionary parametersValues, out bool canCache) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + Check.NotNull(parametersValues, nameof(parametersValues)); + + return new SqlServerSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(selectExpression, parametersValues, out canCache); + } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 9a2fb269e95..93bfb01d9af 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -101,69 +101,99 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression) /// 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. /// - protected override Expression VisitExtension(Expression extensionExpression) + protected virtual Expression VisitSqlServerAggregateFunction(SqlServerAggregateFunctionExpression aggregateFunctionExpression) { - if (extensionExpression is TableExpression tableExpression - && tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationType) != null) + Sql.Append(aggregateFunctionExpression.Name); + + Sql.Append("("); + GenerateList(aggregateFunctionExpression.Arguments, e => Visit(e)); + Sql.Append(")"); + + if (aggregateFunctionExpression.Orderings.Count > 0) { - Sql.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableExpression.Name, tableExpression.Schema)) - .Append(" FOR SYSTEM_TIME "); + Sql.Append(" WITHIN GROUP (ORDER BY "); + GenerateList(aggregateFunctionExpression.Orderings, e => Visit(e)); + Sql.Append(")"); + } - var temporalOperationType = (TemporalOperationType)tableExpression[SqlServerAnnotationNames.TemporalOperationType]!; + return aggregateFunctionExpression; + } - switch (temporalOperationType) + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitExtension(Expression extensionExpression) + { + switch (extensionExpression) + { + case TableExpression tableExpression + when tableExpression.FindAnnotation(SqlServerAnnotationNames.TemporalOperationType) != null: { - case TemporalOperationType.All: - Sql.Append("ALL"); - break; - - case TemporalOperationType.AsOf: - var pointInTime = (DateTime)tableExpression[SqlServerAnnotationNames.TemporalAsOfPointInTime]!; - - Sql.Append("AS OF ") - .Append(_typeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(pointInTime)); - break; - - case TemporalOperationType.Between: - case TemporalOperationType.ContainedIn: - case TemporalOperationType.FromTo: - var from = _typeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral( - (DateTime)tableExpression[SqlServerAnnotationNames.TemporalRangeOperationFrom]!); - - var to = _typeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral( - (DateTime)tableExpression[SqlServerAnnotationNames.TemporalRangeOperationTo]!); - - switch (temporalOperationType) - { - case TemporalOperationType.FromTo: - Sql.Append($"FROM {from} TO {to}"); - break; - - case TemporalOperationType.Between: - Sql.Append($"BETWEEN {from} AND {to}"); - break; - - case TemporalOperationType.ContainedIn: - Sql.Append($"CONTAINED IN ({from}, {to})"); - break; - - default: - throw new InvalidOperationException(tableExpression.Print()); - } - - break; - - default: - throw new InvalidOperationException(tableExpression.Print()); - } + Sql.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableExpression.Name, tableExpression.Schema)) + .Append(" FOR SYSTEM_TIME "); - if (tableExpression.Alias != null) - { - Sql.Append(AliasSeparator) - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableExpression.Alias)); + var temporalOperationType = (TemporalOperationType)tableExpression[SqlServerAnnotationNames.TemporalOperationType]!; + + switch (temporalOperationType) + { + case TemporalOperationType.All: + Sql.Append("ALL"); + break; + + case TemporalOperationType.AsOf: + var pointInTime = (DateTime)tableExpression[SqlServerAnnotationNames.TemporalAsOfPointInTime]!; + + Sql.Append("AS OF ") + .Append(_typeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(pointInTime)); + break; + + case TemporalOperationType.Between: + case TemporalOperationType.ContainedIn: + case TemporalOperationType.FromTo: + var from = _typeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral( + (DateTime)tableExpression[SqlServerAnnotationNames.TemporalRangeOperationFrom]!); + + var to = _typeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral( + (DateTime)tableExpression[SqlServerAnnotationNames.TemporalRangeOperationTo]!); + + switch (temporalOperationType) + { + case TemporalOperationType.FromTo: + Sql.Append($"FROM {from} TO {to}"); + break; + + case TemporalOperationType.Between: + Sql.Append($"BETWEEN {from} AND {to}"); + break; + + case TemporalOperationType.ContainedIn: + Sql.Append($"CONTAINED IN ({from}, {to})"); + break; + + default: + throw new InvalidOperationException(tableExpression.Print()); + } + + break; + + default: + throw new InvalidOperationException(tableExpression.Print()); + } + + if (tableExpression.Alias != null) + { + Sql.Append(AliasSeparator) + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableExpression.Alias)); + } + + return tableExpression; } - return tableExpression; + case SqlServerAggregateFunctionExpression aggregateFunctionExpression: + return VisitSqlServerAggregateFunction(aggregateFunctionExpression); } return base.VisitExtension(extensionExpression); @@ -179,4 +209,22 @@ protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable); } } + + private void GenerateList( + IReadOnlyList items, + Action generationAction, + Action? joinAction = null) + { + joinAction ??= (isb => isb.Append(", ")); + + for (var i = 0; i < items.Count; i++) + { + if (i > 0) + { + joinAction(Sql); + } + + generationAction(items[i]); + } + } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs index 3de428587f3..4e31245166e 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// public class SqlServerSqlExpressionFactory : SqlExpressionFactory { - private IRelationalTypeMappingSource _typeMappingSource; + private readonly IRelationalTypeMappingSource _typeMappingSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -43,9 +43,13 @@ public SqlServerSqlExpressionFactory(SqlExpressionFactoryDependencies dependenci return sqlExpression; } - return sqlExpression is AtTimeZoneExpression atTimeZoneExpression - ? ApplyTypeMappingOnAtTimeZone(atTimeZoneExpression, typeMapping) - : base.ApplyTypeMapping(sqlExpression, typeMapping); + return sqlExpression switch + { + AtTimeZoneExpression e => ApplyTypeMappingOnAtTimeZone(e, typeMapping), + SqlServerAggregateFunctionExpression e => e.ApplyTypeMapping(typeMapping), + + _ => base.ApplyTypeMapping(sqlExpression, typeMapping) + }; } private SqlExpression ApplyTypeMappingOnAtTimeZone(AtTimeZoneExpression atTimeZoneExpression, RelationalTypeMapping? typeMapping) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs new file mode 100644 index 00000000000..581d699f869 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerSqlNullabilityProcessor( + RelationalParameterBasedSqlProcessorDependencies dependencies, + bool useRelationalNulls) + : base(dependencies, useRelationalNulls) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override SqlExpression VisitCustomSqlExpression( + SqlExpression sqlExpression, + bool allowOptimizedExpansion, + out bool nullable) + => sqlExpression switch + { + SqlServerAggregateFunctionExpression aggregateFunctionExpression + => VisitSqlServerAggregateFunction(aggregateFunctionExpression, allowOptimizedExpansion, out nullable), + + _ => base.VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) + }; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual SqlExpression VisitSqlServerAggregateFunction( + SqlServerAggregateFunctionExpression aggregateFunctionExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + nullable = aggregateFunctionExpression.IsNullable; + + SqlExpression[]? arguments = null; + for (var i = 0; i < aggregateFunctionExpression.Arguments.Count; i++) + { + var visitedArgument = Visit(aggregateFunctionExpression.Arguments[i], out _); + if (visitedArgument != aggregateFunctionExpression.Arguments[i] && arguments is null) + { + arguments = new SqlExpression[aggregateFunctionExpression.Arguments.Count]; + + for (var j = 0; j < i; j++) + { + arguments[j] = aggregateFunctionExpression.Arguments[j]; + } + } + + if (arguments is not null) + { + arguments[i] = visitedArgument; + } + } + + OrderingExpression[]? orderings = null; + for (var i = 0; i < aggregateFunctionExpression.Orderings.Count; i++) + { + var ordering = aggregateFunctionExpression.Orderings[i]; + var visitedOrdering = ordering.Update(Visit(ordering.Expression, out _)); + if (visitedOrdering != aggregateFunctionExpression.Orderings[i] && orderings is null) + { + orderings = new OrderingExpression[aggregateFunctionExpression.Orderings.Count]; + + for (var j = 0; j < i; j++) + { + orderings[j] = aggregateFunctionExpression.Orderings[j]; + } + } + + if (orderings is not null) + { + orderings[i] = visitedOrdering; + } + } + + return arguments is not null || orderings is not null + ? aggregateFunctionExpression.Update( + arguments ?? aggregateFunctionExpression.Arguments, + orderings ?? aggregateFunctionExpression.Orderings) + : aggregateFunctionExpression; + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerStatisticsAggregateMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerStatisticsAggregateMethodTranslator.cs new file mode 100644 index 00000000000..8487e541c52 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerStatisticsAggregateMethodTranslator.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerStatisticsAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly RelationalTypeMapping _doubleTypeMapping; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerStatisticsAggregateMethodTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _doubleTypeMapping = typeMappingSource.FindMapping(typeof(double))!; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression? Translate( + MethodInfo method, EnumerableExpression source, IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Docs: https://docs.microsoft.com/sql/t-sql/functions/aggregate-functions-transact-sql + + if (method.DeclaringType != typeof(SqlServerDbFunctionsExtensions) + || source.Selector is not SqlExpression sqlExpression) + { + return null; + } + + var functionName = method.Name switch + { + nameof(SqlServerDbFunctionsExtensions.StandardDeviationSample) => "STDEV", + nameof(SqlServerDbFunctionsExtensions.StandardDeviationPopulation) => "STDEVP", + nameof(SqlServerDbFunctionsExtensions.VarianceSample) => "VAR", + nameof(SqlServerDbFunctionsExtensions.VariancePopulation) => "VARP", + _ => null + }; + + if (functionName is null) + { + return null; + } + + return SqlServerExpression.AggregateFunction( + _sqlExpressionFactory, + functionName, + new[] { sqlExpression }, + source, + enumerableArgumentIndex: 0, + nullable: true, + argumentsPropagateNullability: new[] { false }, + typeof(double), + _doubleTypeMapping); + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerStringAggregateMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerStringAggregateMethodTranslator.cs new file mode 100644 index 00000000000..d4ef723a318 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerStringAggregateMethodTranslator.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerStringAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private static readonly MethodInfo StringConcatMethod + = typeof(string).GetRuntimeMethod(nameof(string.Concat), new[] { typeof(IEnumerable) })!; + + private static readonly MethodInfo StringJoinMethod + = typeof(string).GetRuntimeMethod(nameof(string.Join), new[] { typeof(string), typeof(IEnumerable) })!; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalTypeMappingSource _typeMappingSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerStringAggregateMethodTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + { + _sqlExpressionFactory = sqlExpressionFactory; + _typeMappingSource = typeMappingSource; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression? Translate( + MethodInfo method, EnumerableExpression source, IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Docs: https://docs.microsoft.com/sql/t-sql/functions/string-agg-transact-sql + + if (source.Selector is not SqlExpression sqlExpression + || (method != StringJoinMethod && method != StringConcatMethod)) + { + return null; + } + + // STRING_AGG enlarges the return type size (e.g. for input VARCHAR(5), it returns VARCHAR(8000)). + // See https://docs.microsoft.com/sql/t-sql/functions/string-agg-transact-sql#return-types + var resultTypeMapping = sqlExpression.TypeMapping; + if (resultTypeMapping?.Size != null) + { + if (resultTypeMapping.IsUnicode && resultTypeMapping.Size < 8000) + { + resultTypeMapping = _typeMappingSource.FindMapping( + typeof(string), + resultTypeMapping.StoreTypeNameBase, + unicode: true, + size: 8000); + } + else if (!resultTypeMapping.IsUnicode && resultTypeMapping.Size < 4000) + { + resultTypeMapping = _typeMappingSource.FindMapping( + typeof(string), + resultTypeMapping.StoreTypeNameBase, + unicode: false, + size: 4000); + } + } + + // STRING_AGG filters out nulls, but string.Join treats them as empty strings; coalesce unless we know we're aggregating over + // a non-nullable column. + if (sqlExpression is not ColumnExpression { IsNullable: false }) + { + sqlExpression = _sqlExpressionFactory.Coalesce( + sqlExpression, + _sqlExpressionFactory.Constant(string.Empty, typeof(string))); + } + + // STRING_AGG returns null when there are no rows (or non-null values), but string.Join returns an empty string. + return + _sqlExpressionFactory.Coalesce( + SqlServerExpression.AggregateFunctionWithOrdering( + _sqlExpressionFactory, + "STRING_AGG", + new[] + { + sqlExpression, + _sqlExpressionFactory.ApplyTypeMapping( + method == StringJoinMethod ? arguments[0] : _sqlExpressionFactory.Constant(string.Empty, typeof(string)), + sqlExpression.TypeMapping) + }, + source, + enumerableArgumentIndex: 0, + nullable: true, + argumentsPropagateNullability: new[] { false, true }, + typeof(string)), + _sqlExpressionFactory.Constant(string.Empty, typeof(string)), + resultTypeMapping); + } +} diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteAggregateMethodCallTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteAggregateMethodCallTranslatorProvider.cs index a836f543a5b..c67a66cc171 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteAggregateMethodCallTranslatorProvider.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteAggregateMethodCallTranslatorProvider.cs @@ -25,7 +25,8 @@ public SqliteAggregateMethodCallTranslatorProvider(RelationalAggregateMethodCall AddTranslators( new IAggregateMethodCallTranslator[] { - new SqliteQueryableAggregateMethodTranslator(sqlExpressionFactory) + new SqliteQueryableAggregateMethodTranslator(sqlExpressionFactory), + new SqliteStringAggregateMethodTranslator(sqlExpressionFactory) }); } } diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteStringAggregateMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteStringAggregateMethodTranslator.cs new file mode 100644 index 00000000000..f5a467880f6 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteStringAggregateMethodTranslator.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqliteStringAggregateMethodTranslator : IAggregateMethodCallTranslator +{ + private static readonly MethodInfo StringConcatMethod + = typeof(string).GetRuntimeMethod(nameof(string.Concat), new[] { typeof(IEnumerable) })!; + + private static readonly MethodInfo StringJoinMethod + = typeof(string).GetRuntimeMethod(nameof(string.Join), new[] { typeof(string), typeof(IEnumerable) })!; + + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqliteStringAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + => _sqlExpressionFactory = sqlExpressionFactory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual SqlExpression? Translate( + MethodInfo method, EnumerableExpression source, IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + // Docs: https://sqlite.org/lang_aggfunc.html#group_concat + + if (source.Selector is not SqlExpression sqlExpression + || (method != StringJoinMethod && method != StringConcatMethod)) + { + return null; + } + + // SQLite does not support input ordering on aggregate methods. Since ordering matters very much for translating, if the user + // specified an ordering we refuse to translate (but to error than to ignore in this case). + if (source.Orderings.Count > 0) + { + return null; + } + + if (sqlExpression is not ColumnExpression { IsNullable: false }) + { + sqlExpression = _sqlExpressionFactory.Coalesce( + sqlExpression, + _sqlExpressionFactory.Constant(string.Empty, typeof(string))); + } + + if (source.Predicate != null) + { + if (sqlExpression is SqlFragmentExpression) + { + sqlExpression = _sqlExpressionFactory.Constant(1); + } + + sqlExpression = _sqlExpressionFactory.Case( + new List { new(source.Predicate, sqlExpression) }, + elseResult: null); + } + + if (source.IsDistinct) + { + sqlExpression = new DistinctExpression(sqlExpression); + } + + // group_concat returns null when there are no rows (or non-null values), but string.Join returns an empty string. + return _sqlExpressionFactory.Coalesce( + _sqlExpressionFactory.Function( + "group_concat", + new[] + { + sqlExpression, + _sqlExpressionFactory.ApplyTypeMapping( + method == StringJoinMethod ? arguments[0] : _sqlExpressionFactory.Constant(string.Empty, typeof(string)), + sqlExpression.TypeMapping) + }, + nullable: true, + argumentsPropagateNullability: new[] { false, true }, + typeof(string)), + _sqlExpressionFactory.Constant(string.Empty, typeof(string)), + sqlExpression.TypeMapping); + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs index a67e1e4ced6..f8aa3238207 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindFunctionsQueryCosmosTest.cs @@ -1447,6 +1447,21 @@ FROM root c WHERE ((c[""Discriminator""] = ""OrderDetail"") AND (c[""Quantity""] < 5))"); } + public override Task String_Join_over_non_nullable_column(bool async) + => AssertTranslationFailed(() => base.String_Join_over_non_nullable_column(async)); + + public override Task String_Join_with_predicate(bool async) + => AssertTranslationFailed(() => base.String_Join_with_predicate(async)); + + public override Task String_Join_with_ordering(bool async) + => AssertTranslationFailed(() => base.String_Join_with_ordering(async)); + + public override Task String_Join_over_nullable_column(bool async) + => AssertTranslationFailed(() => base.String_Join_over_nullable_column(async)); + + public override Task String_Concat(bool async) + => AssertTranslationFailed(() => base.String_Concat(async)); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs index d2da16bccf9..a2a97cd9ebf 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindFunctionsQueryTestBase.cs @@ -161,6 +161,111 @@ public virtual Task String_Contains_MethodCall(bool async) ss => ss.Set().Where(c => c.ContactName.Contains(LocalMethod1())), entryCount: 19); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Join_over_non_nullable_column(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(c => c.City) + .Select(g => new + { + City = g.Key, + Customers = string.Join("|", g.Select(e => e.CustomerID)) + }), + elementSorter: x => x.City, + elementAsserter: (e, a) => + { + Assert.Equal(e.City, a.City); + + // Ordering inside the string isn't specified server-side, split and reorder + Assert.Equal( + e.Customers.Split("|").OrderBy(id => id).ToArray(), + a.Customers.Split("|").OrderBy(id => id).ToArray()); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Join_with_predicate(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(c => c.City) + .Select(g => new + { + City = g.Key, + Customers = string.Join("|", g.Where(e => e.ContactName.Length > 10).Select(e => e.CustomerID)) + }), + elementSorter: x => x.City, + elementAsserter: (e, a) => + { + Assert.Equal(e.City, a.City); + + // Ordering inside the string isn't specified server-side, split and reorder + Assert.Equal( + e.Customers.Split("|").OrderBy(id => id).ToArray(), + a.Customers.Split("|").OrderBy(id => id).ToArray()); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Join_with_ordering(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(c => c.City) + .Select(g => new + { + City = g.Key, + Customers = string.Join("|", g.OrderByDescending(e => e.CustomerID).Select(e => e.CustomerID)) + }), + elementSorter: x => x.City); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Join_over_nullable_column(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(c => c.City) + .Select(g => new + { + City = g.Key, + Regions = string.Join("|", g.Select(e => e.Region)) + }), + elementSorter: x => x.City, + elementAsserter: (e, a) => + { + Assert.Equal(e.City, a.City); + + // Ordering inside the string isn't specified server-side, split and reorder + Assert.Equal( + e.Regions.Split("|").OrderBy(id => id).ToArray(), + a.Regions.Split("|").OrderBy(id => id).ToArray()); + }); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task String_Concat(bool async) + => AssertQuery( + async, + ss => ss.Set() + .GroupBy(c => c.City) + .Select(g => new + { + City = g.Key, + Customers = string.Concat(g.Select(e => e.CustomerID)) + }), + elementSorter: x => x.City, + elementAsserter: (e, a) => + { + Assert.Equal(e.City, a.City); + + // The best we can do for Concat without server-side ordering is sort the characters (concatenating without ordering + // and without a delimiter is somewhat dubious anyway). + Assert.Equal(e.Customers.OrderBy(c => c).ToArray(), a.Customers.OrderBy(c => c).ToArray()); + }); + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task String_Compare_simple_zero(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs index 8b58746c66d..b758105149f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindFunctionsQuerySqlServerTest.cs @@ -238,6 +238,63 @@ FROM [Customers] AS [c] WHERE [c].[ContactName] LIKE N'%M%'"); } + [SqlServerCondition(SqlServerCondition.SupportsFunctions2017)] + public override async Task String_Join_over_non_nullable_column(bool async) + { + await base.String_Join_over_non_nullable_column(async); + + AssertSql( + @"SELECT [c].[City], COALESCE(STRING_AGG([c].[CustomerID], N'|'), N'') AS [Customers] +FROM [Customers] AS [c] +GROUP BY [c].[City]"); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2017)] + public override async Task String_Join_over_nullable_column(bool async) + { + await base.String_Join_over_nullable_column(async); + + AssertSql( + @"SELECT [c].[City], COALESCE(STRING_AGG(COALESCE([c].[Region], N''), N'|'), N'') AS [Regions] +FROM [Customers] AS [c] +GROUP BY [c].[City]"); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2017)] + public override async Task String_Join_with_predicate(bool async) + { + await base.String_Join_with_predicate(async); + + AssertSql( + @"SELECT [c].[City], COALESCE(STRING_AGG(CASE + WHEN CAST(LEN([c].[ContactName]) AS int) > 10 THEN [c].[CustomerID] +END, N'|'), N'') AS [Customers] +FROM [Customers] AS [c] +GROUP BY [c].[City]"); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2017)] + public override async Task String_Join_with_ordering(bool async) + { + await base.String_Join_with_ordering(async); + + AssertSql( + @"SELECT [c].[City], COALESCE(STRING_AGG([c].[CustomerID], N'|') WITHIN GROUP (ORDER BY [c].[CustomerID] DESC), N'') AS [Customers] +FROM [Customers] AS [c] +GROUP BY [c].[City]"); + } + + [SqlServerCondition(SqlServerCondition.SupportsFunctions2017)] + public override async Task String_Concat(bool async) + { + await base.String_Concat(async); + + AssertSql( + @"SELECT [c].[City], COALESCE(STRING_AGG([c].[CustomerID], N''), N'') AS [Customers] +FROM [Customers] AS [c] +GROUP BY [c].[City]"); + } + public override async Task String_Compare_simple_zero(bool async) { await base.String_Compare_simple_zero(async); @@ -2009,6 +2066,65 @@ public override Task Regex_IsMatch_MethodCall_constant_input(bool async) public override Task Datetime_subtraction_TotalDays(bool async) => AssertTranslationFailed(() => base.Datetime_subtraction_TotalDays(async)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task StandardDeviation(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(od => od.ProductID) + .Select(g => new + { + ProductID = g.Key, + SampleStandardDeviation = EF.Functions.StandardDeviationSample(g.Select(od => od.UnitPrice)), + PopulationStandardDeviation = EF.Functions.StandardDeviationPopulation(g.Select(od => od.UnitPrice)) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var product9 = results.Single(r => r.ProductID == 9); + Assert.Equal(8.675943752699023, product9.SampleStandardDeviation.Value, 5); + Assert.Equal(7.759999999999856, product9.PopulationStandardDeviation.Value, 5); + + AssertSql( + @"SELECT [o].[ProductID], STDEV([o].[UnitPrice]) AS [SampleStandardDeviation], STDEVP([o].[UnitPrice]) AS [PopulationStandardDeviation] +FROM [Order Details] AS [o] +GROUP BY [o].[ProductID]"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Variance(bool async) + { + await using var ctx = CreateContext(); + + var query = ctx.Set() + .GroupBy(od => od.ProductID) + .Select( + g => new + { + ProductID = g.Key, + SampleStandardDeviation = EF.Functions.VarianceSample(g.Select(od => od.UnitPrice)), + PopulationStandardDeviation = EF.Functions.VariancePopulation(g.Select(od => od.UnitPrice)) + }); + + var results = async + ? await query.ToListAsync() + : query.ToList(); + + var product9 = results.Single(r => r.ProductID == 9); + Assert.Equal(75.2719999999972, product9.SampleStandardDeviation.Value, 5); + Assert.Equal(60.217599999997766, product9.PopulationStandardDeviation.Value, 5); + + AssertSql( + @"SELECT [o].[ProductID], VAR([o].[UnitPrice]) AS [SampleStandardDeviation], VARP([o].[UnitPrice]) AS [PopulationStandardDeviation] +FROM [Order Details] AS [o] +GROUP BY [o].[ProductID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs index 5466a13c8bd..c1fa32d4e62 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs @@ -15,5 +15,7 @@ public enum SqlServerCondition SupportsFullTextSearch = 1 << 6, SupportsOnlineIndexes = 1 << 7, SupportsTemporalTablesCascadeDelete = 1 << 8, - SupportsUtf8 = 1 << 9 + SupportsUtf8 = 1 << 9, + SupportsFunctions2019 = 1 << 10, + SupportsFunctions2017 = 1 << 11 } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs index d6d9a9882ad..c4f50b7552d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs @@ -72,6 +72,16 @@ public ValueTask IsMetAsync() isMet &= TestEnvironment.IsUtf8Supported; } + if (Conditions.HasFlag(SqlServerCondition.SupportsFunctions2019)) + { + isMet &= TestEnvironment.IsFunctions2019Supported; + } + + if (Conditions.HasFlag(SqlServerCondition.SupportsFunctions2017)) + { + isMet &= TestEnvironment.IsFunctions2017Supported; + } + return new ValueTask(isMet); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs index 64d6392f9bf..2320b24b7bb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs @@ -39,6 +39,10 @@ public static class TestEnvironment private static bool? _supportsUtf8; + private static bool? _supportsFunctions2017; + + private static bool? _supportsFunctions2019; + private static byte? _productMajorVersion; private static int? _engineEdition; @@ -265,6 +269,67 @@ public static bool IsUtf8Supported } } + public static bool IsFunctions2017Supported + { + get + { + if (!IsConfigured) + { + return false; + } + + if (_supportsFunctions2017.HasValue) + { + return _supportsFunctions2017.Value; + } + + try + { + _productMajorVersion = GetProductMajorVersion(); + + _supportsFunctions2017 = _productMajorVersion >= 14 || IsSqlAzure; + } + catch (PlatformNotSupportedException) + { + _supportsFunctions2017 = false; + } + + return _supportsFunctions2017.Value; + } + } + + public static bool IsFunctions2019Supported + { + get + { + if (!IsConfigured) + { + return false; + } + + if (_supportsFunctions2019.HasValue) + { + return _supportsFunctions2019.Value; + } + + try + { + _productMajorVersion = GetProductMajorVersion(); + + _supportsFunctions2019 = _productMajorVersion >= 15 || IsSqlAzure; + } + catch (PlatformNotSupportedException) + { + _supportsFunctions2019 = false; + } + + return _supportsFunctions2019.Value; + } + } + + public static byte SqlServerMajorVersion + => GetProductMajorVersion(); + public static string ElasticPoolName { get; } = Config["ElasticPoolName"]; public static bool? GetFlag(string key) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index 3e79290721e..0185d15146a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.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 class NorthwindFunctionsQuerySqliteTest : NorthwindFunctionsQueryRelationalTestBase< @@ -324,6 +326,64 @@ public override async Task String_Contains_MethodCall(bool async) WHERE 'M' = '' OR instr(""c"".""ContactName"", 'M') > 0"); } + public override async Task String_Join_over_non_nullable_column(bool async) + { + await base.String_Join_over_non_nullable_column(async); + + AssertSql( + @"SELECT ""c"".""City"", COALESCE(group_concat(""c"".""CustomerID"", '|'), '') AS ""Customers"" +FROM ""Customers"" AS ""c"" +GROUP BY ""c"".""City"""); + } + + public override async Task String_Join_over_nullable_column(bool async) + { + await base.String_Join_over_nullable_column(async); + + AssertSql( + @"SELECT ""c"".""City"", COALESCE(group_concat(COALESCE(""c"".""Region"", ''), '|'), '') AS ""Regions"" +FROM ""Customers"" AS ""c"" +GROUP BY ""c"".""City"""); + } + + public override async Task String_Join_with_predicate(bool async) + { + await base.String_Join_with_predicate(async); + + AssertSql( + @"SELECT ""c"".""City"", COALESCE(group_concat(CASE + WHEN length(""c"".""ContactName"") > 10 THEN ""c"".""CustomerID"" +END, '|'), '') AS ""Customers"" +FROM ""Customers"" AS ""c"" +GROUP BY ""c"".""City"""); + } + + public override async Task String_Join_with_ordering(bool async) + { + // SQLite does not support input ordering on aggregate methods; the below does client evaluation. + await base.String_Join_with_ordering(async); + + AssertSql( + @"SELECT ""t"".""City"", ""c0"".""CustomerID"" +FROM ( + SELECT ""c"".""City"" + FROM ""Customers"" AS ""c"" + GROUP BY ""c"".""City"" +) AS ""t"" +LEFT JOIN ""Customers"" AS ""c0"" ON ""t"".""City"" = ""c0"".""City"" +ORDER BY ""t"".""City"", ""c0"".""CustomerID"" DESC"); + } + + public override async Task String_Concat(bool async) + { + await base.String_Concat(async); + + AssertSql( + @"SELECT ""c"".""City"", COALESCE(group_concat(""c"".""CustomerID"", ''), '') AS ""Customers"" +FROM ""Customers"" AS ""c"" +GROUP BY ""c"".""City"""); + } + public override async Task IsNullOrWhiteSpace_in_predicate(bool async) { await base.IsNullOrWhiteSpace_in_predicate(async);