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);