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.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/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..bab683eaf7d
--- /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 SqlFunctionExpression 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 SqlServerSqlFunctionExpression(
+ 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..d593e7e83e4 100644
--- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs
@@ -95,6 +95,37 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression)
}
}
+ ///
+ /// 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 VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression)
+ {
+ base.VisitSqlFunction(sqlFunctionExpression);
+
+ if (sqlFunctionExpression is SqlServerSqlFunctionExpression sqlServerFunctionExpression
+ && sqlServerFunctionExpression.AggregateOrderings.Count > 0)
+ {
+ Sql.Append(" WITHIN GROUP (ORDER BY ");
+
+ for (var i = 0; i < sqlServerFunctionExpression.AggregateOrderings.Count; i++)
+ {
+ if (i > 0)
+ {
+ Sql.Append(", ");
+ }
+
+ Visit(sqlServerFunctionExpression.AggregateOrderings[i]);
+ }
+
+ Sql.Append(")");
+ }
+
+ return sqlFunctionExpression;
+ }
+
///
/// 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
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlExpressionFactory.cs
index 3de428587f3..42b334385f8 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
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlFunctionExpression.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlFunctionExpression.cs
new file mode 100644
index 00000000000..ab0a8bec3ac
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlFunctionExpression.cs
@@ -0,0 +1,165 @@
+// 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 SqlServerSqlFunctionExpression : SqlFunctionExpression, IEquatable
+{
+ ///
+ /// 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 SqlServerSqlFunctionExpression(
+ string functionName,
+ IEnumerable arguments,
+ IReadOnlyList aggregateOrderings,
+ bool nullable,
+ IEnumerable argumentsPropagateNullability,
+ Type type,
+ RelationalTypeMapping? typeMapping)
+ : base(functionName, arguments, nullable, argumentsPropagateNullability, type, typeMapping)
+ => AggregateOrderings = aggregateOrderings;
+
+ ///
+ /// 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 AggregateOrderings { 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)
+ {
+ var visitedBase = (SqlFunctionExpression)base.VisitChildren(visitor);
+
+ OrderingExpression[]? visitedAggregateOrderings = null;
+
+ for (var i = 0; i < AggregateOrderings.Count; i++)
+ {
+ var visitedOrdering = (OrderingExpression)visitor.Visit(AggregateOrderings[i]);
+ if (visitedOrdering != AggregateOrderings[i] && visitedAggregateOrderings is null)
+ {
+ visitedAggregateOrderings = new OrderingExpression[AggregateOrderings.Count];
+
+ for (var j = 0; j < visitedAggregateOrderings.Length; j++)
+ {
+ visitedAggregateOrderings[j] = AggregateOrderings[j];
+ }
+ }
+
+ if (visitedAggregateOrderings is not null)
+ {
+ visitedAggregateOrderings[i] = visitedOrdering;
+ }
+ }
+
+ return visitedBase != this || visitedAggregateOrderings is not null
+ ? new SqlServerSqlFunctionExpression(
+ Name,
+ visitedBase.Arguments!,
+ visitedAggregateOrderings ?? AggregateOrderings,
+ 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 override SqlServerSqlFunctionExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping)
+ => new(
+ Name,
+ Arguments!,
+ AggregateOrderings,
+ 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 override SqlFunctionExpression Update(SqlExpression? instance, IReadOnlyList? arguments)
+ {
+ Check.DebugAssert(arguments is not null, "arguments is not null");
+ Check.DebugAssert(instance is null, "instance not supported on SqlServerFunctionExpression");
+
+ return arguments.SequenceEqual(Arguments!)
+ ? this
+ : new SqlServerSqlFunctionExpression(
+ Name, arguments, AggregateOrderings, IsNullable, ArgumentsPropagateNullability!, Type, 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 SqlFunctionExpression UpdateAggregateOrderings(IReadOnlyList aggregateOrderings)
+ => aggregateOrderings.SequenceEqual(AggregateOrderings)
+ ? this
+ : new SqlServerSqlFunctionExpression(
+ Name, Arguments!, aggregateOrderings, IsNullable, ArgumentsPropagateNullability!, Type, TypeMapping);
+
+ ///
+ protected override void Print(ExpressionPrinter expressionPrinter)
+ {
+ base.Print(expressionPrinter);
+
+ if (AggregateOrderings.Count > 0)
+ {
+ expressionPrinter.Append(" WITHIN GROUP (ORDER BY ");
+ expressionPrinter.VisitCollection(AggregateOrderings);
+ expressionPrinter.Append(")");
+ }
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ => obj is SqlServerSqlFunctionExpression sqlServerFunctionExpression && Equals(sqlServerFunctionExpression);
+
+ ///
+ public virtual bool Equals(SqlServerSqlFunctionExpression? other)
+ => ReferenceEquals(this, other)
+ || base.Equals(other) && AggregateOrderings.SequenceEqual(other.AggregateOrderings);
+
+ ///
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+
+ hash.Add(base.GetHashCode());
+
+ foreach (var orderingExpression in AggregateOrderings)
+ {
+ hash.Add(orderingExpression);
+ }
+
+ return hash.ToHashCode();
+ }
+}
diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs
new file mode 100644
index 00000000000..2e78de986bb
--- /dev/null
+++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs
@@ -0,0 +1,75 @@
+// 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 VisitSqlFunction(
+ SqlFunctionExpression sqlFunctionExpression,
+ bool allowOptimizedExpansion,
+ out bool nullable)
+ {
+ var visitedBase = base.VisitSqlFunction(sqlFunctionExpression, allowOptimizedExpansion, out nullable);
+
+ if (visitedBase is SqlServerSqlFunctionExpression sqlServerSqlFunctionExpression)
+ {
+ var aggregateOrderings = sqlServerSqlFunctionExpression.AggregateOrderings;
+ OrderingExpression[]? visitedAggregateOrderings = null;
+
+ for (var i = 0; i < aggregateOrderings.Count; i++)
+ {
+ var ordering = aggregateOrderings[i];
+ var visitedOrdering = ordering.Update(Visit(ordering.Expression, out _));
+ if (visitedOrdering != aggregateOrderings[i] && visitedAggregateOrderings is null)
+ {
+ visitedAggregateOrderings = new OrderingExpression[aggregateOrderings.Count];
+
+ for (var j = 0; j < visitedAggregateOrderings.Length; j++)
+ {
+ visitedAggregateOrderings[j] = aggregateOrderings[j];
+ }
+ }
+
+ if (visitedAggregateOrderings is not null)
+ {
+ visitedAggregateOrderings[i] = visitedOrdering;
+ }
+ }
+
+ if (visitedAggregateOrderings is not null)
+ {
+ return sqlServerSqlFunctionExpression.UpdateAggregateOrderings(visitedAggregateOrderings);
+ }
+ }
+
+ return visitedBase;
+ }
+}
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.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..9148b0e193f 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.Version2017)]
+ 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.Version2017)]
+ 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.Version2017)]
+ 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.Version2017)]
+ 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.Version2017)]
+ 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..8a1a1a11513 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,
+ Version2019 = 1 << 10,
+ Version2017 = 1 << 11
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs
index d6d9a9882ad..81084da2942 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.Version2019))
+ {
+ isMet &= TestEnvironment.SqlServerMajorVersion >= 15;
+ }
+
+ if (Conditions.HasFlag(SqlServerCondition.Version2017))
+ {
+ isMet &= TestEnvironment.SqlServerMajorVersion >= 14;
+ }
+
return new ValueTask(isMet);
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs
index 64d6392f9bf..bc276e97f5b 100644
--- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs
@@ -265,6 +265,9 @@ public static bool IsUtf8Supported
}
}
+ 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);