From 3108ce3eb20541071584e766f32dda623fd7fadd Mon Sep 17 00:00:00 2001 From: Paul Middleton Date: Sat, 20 Jul 2024 12:08:52 -0500 Subject: [PATCH 1/3] WIP --- ...ntityFrameworkRelationalServicesBuilder.cs | 9 +- .../Query/ISqlExpressionFactory.cs | 54 + .../IWindowAggregateMethodCallTranslator.cs | 29 + ...ndowAggregateMethodCallTranslatorPlugin.cs | 21 + .../Query/IWindowBuilderExpressionFactory.cs | 22 + .../Query/QuerySqlGenerator.cs | 94 + .../RelationalEvaluatableExpressionFilter.cs | 3 +- ...lationalSqlTranslatingExpressionVisitor.cs | 117 +- ...ranslatingExpressionVisitorDependencies.cs | 16 +- ...tionalWindowAggregateFunctionExtensions.cs | 332 ++++ ...lationalWindowAggregateMethodTranslator.cs | 196 +++ .../Query/RelationalWindowAggregateMethods.cs | 68 + ...elationalWindowBuilderExpressionFactory.cs | 87 + .../Query/SqlExpressionFactory.cs | 99 ++ .../Query/SqlExpressionVisitor.cs | 24 + .../SqlExpressions/WindowFrameExpression.cs | 151 ++ .../SqlExpressions/WindowOverExpression.cs | 193 ++ .../WindowPartitionExpression.cs | 94 + .../Query/SqlNullabilityProcessor.cs | 20 + .../Query/WindowBuilderExpressionFactory.cs | 36 + .../Query/WindowFunctionInterfaces.cs | 198 +++ .../Query/WindowFunctionsExtensions.cs | 26 + .../SqlServerServiceCollectionExtensions.cs | 2 + ...ServerWindowAggregateFunctionExtensions.cs | 176 ++ ...qlServerWindowAggregateMethodTranslator.cs | 114 ++ .../SqlServerWindowAggregateMethods.cs | 54 + .../SqliteSqlTranslatingExpressionVisitor.cs | 41 +- .../Query/SqliteWindowFunctionExtensions.cs | 117 ++ .../Query/WindowFunctionTestBase.cs | 1562 +++++++++++++++++ .../Query/WindowFunctionSqlServerTest.cs | 1247 +++++++++++++ .../Query/WindowFunctionSqliteTest.cs | 1407 +++++++++++++++ 31 files changed, 6602 insertions(+), 7 deletions(-) create mode 100644 src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslator.cs create mode 100644 src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslatorPlugin.cs create mode 100644 src/EFCore.Relational/Query/IWindowBuilderExpressionFactory.cs create mode 100644 src/EFCore.Relational/Query/RelationalWindowAggregateFunctionExtensions.cs create mode 100644 src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs create mode 100644 src/EFCore.Relational/Query/RelationalWindowAggregateMethods.cs create mode 100644 src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs create mode 100644 src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs create mode 100644 src/EFCore.Relational/Query/WindowBuilderExpressionFactory.cs create mode 100644 src/EFCore.Relational/Query/WindowFunctionInterfaces.cs create mode 100644 src/EFCore.Relational/Query/WindowFunctionsExtensions.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateFunctionExtensions.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethods.cs create mode 100644 src/EFCore.Sqlite.Core/Query/SqliteWindowFunctionExtensions.cs create mode 100644 test/EFCore.Relational.Specification.Tests/Query/WindowFunctionTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index d0ee126a44c..ea6aac44893 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -69,6 +69,7 @@ public static readonly IDictionary RelationalServi { typeof(IRelationalSqlTranslatingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMethodCallTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IAggregateMethodCallTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IWindowAggregateMethodCallTranslator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMemberTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(ISqlExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IRelationalQueryStringFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -96,7 +97,8 @@ public static readonly IDictionary RelationalServi typeof(IAggregateMethodCallTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, - { typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) } + { typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, + { typeof(IWindowBuilderExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) } }; /// @@ -179,6 +181,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); TryAdd(); TryAdd(); TryAdd(); @@ -192,6 +195,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(p => p.GetRequiredService()); TryAdd(); TryAdd(); + TryAdd(); ServiceCollectionMap.GetInfrastructure() .AddDependencySingleton() @@ -229,7 +233,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() - .AddDependencyScoped(); + .AddDependencyScoped() + .AddDependencyScoped(); return base.TryAddCoreServices(); } diff --git a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs index 7308836e749..4a93dd53e93 100644 --- a/src/EFCore.Relational/Query/ISqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/ISqlExpressionFactory.cs @@ -437,4 +437,58 @@ SqlExpression NiladicFunction( /// A string token to print in SQL tree. /// An expression representing a SQL token. SqlExpression Fragment(string sql); + + /// + /// Attempts to creates a new expression that returns the smallest value from a list of expressions, e.g. an invocation of the + /// LEAST SQL function. + /// + /// An entity type to project. + /// The result CLR type for the returned expression. + /// The expression which computes the smallest value. + /// if the expression could be created, otherwise. + bool TryCreateLeast( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? leastExpression); + + /// + /// Attempts to creates a new expression that returns the greatest value from a list of expressions, e.g. an invocation of the + /// GREATEST SQL function. + /// + /// An entity type to project. + /// The result CLR type for the returned expression. + /// The expression which computes the greatest value. + /// if the expression could be created, otherwise. + bool TryCreateGreatest( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? greatestExpression); + + /// + /// todo + /// + /// todo + /// todo + WindowPartitionExpression PartitionBy(IEnumerable partitions); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + WindowOverExpression Over(SqlFunctionExpression aggregate, WindowPartitionExpression? partition, IReadOnlyList orderings, + WindowFrameExpression? frame); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + WindowFrameExpression WindowFrame(MethodInfo method, SqlExpression? preceding, SqlExpression? following, SqlExpression? exclude); } diff --git a/src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslator.cs b/src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslator.cs new file mode 100644 index 00000000000..5fbde167c42 --- /dev/null +++ b/src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslator.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public interface IWindowAggregateMethodCallTranslator +{ + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + SqlExpression? Translate( + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger); +} diff --git a/src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslatorPlugin.cs b/src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslatorPlugin.cs new file mode 100644 index 00000000000..bdc193f396a --- /dev/null +++ b/src/EFCore.Relational/Query/IWindowAggregateMethodCallTranslatorPlugin.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public interface IWindowAggregateMethodCallTranslatorPlugin +{ + /// + /// Gets the method call translators. + /// + IEnumerable Translators { get; } +} diff --git a/src/EFCore.Relational/Query/IWindowBuilderExpressionFactory.cs b/src/EFCore.Relational/Query/IWindowBuilderExpressionFactory.cs new file mode 100644 index 00000000000..8452ba2c188 --- /dev/null +++ b/src/EFCore.Relational/Query/IWindowBuilderExpressionFactory.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public interface IWindowBuilderExpressionFactory +{ + /// + /// todo + /// + /// todo + RelationalWindowBuilderExpression CreateWindowBuilder(); +} diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 1906d587a40..d8e2a0aeafa 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Reflection.Metadata; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage.Internal; @@ -1763,4 +1764,97 @@ protected virtual bool TryGetOperatorInfo(SqlExpression expression, out int prec (precedence, isAssociative) = (default, default); return false; } + + /// + protected override Expression VisitOver(WindowOverExpression windowOverExpression) + { + Visit(windowOverExpression.Aggregate); + + _relationalCommandBuilder.Append(" OVER ("); + + if(windowOverExpression.Partition != null) + VisitWindowPartition(windowOverExpression.Partition); + + if (windowOverExpression.Ordering.Count > 0) + { + _relationalCommandBuilder.Append(" ORDER BY "); + + GenerateList(windowOverExpression.Ordering, e => Visit(e)); + } + + if (windowOverExpression.WindowFrame != null) + VisitWindowFrame(windowOverExpression.WindowFrame); + + _relationalCommandBuilder.Append(")"); + + return windowOverExpression; + } + + /// + protected override Expression VisitWindowPartition(WindowPartitionExpression partitionExpression) + { + _relationalCommandBuilder.Append("PARTITION BY "); + + GenerateList(partitionExpression.Partitions, e => Visit(e), sql => sql.Append(", ")); + + return partitionExpression; + } + + /// + protected override Expression VisitWindowFrame(WindowFrameExpression windowsFrameExpression) + { + //todo - sqllite groups override test + + _relationalCommandBuilder.Append($" {windowsFrameExpression.FrameName} "); + + if(windowsFrameExpression.Following != null) + _relationalCommandBuilder.Append($"BETWEEN "); + + if (windowsFrameExpression.Preceding is SqlConstantExpression preceedingExpression && preceedingExpression?.Type == typeof(RowsPreceding)) + { + _relationalCommandBuilder.Append((RowsPreceding)preceedingExpression.Value! == RowsPreceding.CurrentRow + ? "CURRENT ROW" + : "UNBOUNDED PRECEDING"); + } + else + { + Visit(windowsFrameExpression.Preceding); + + _relationalCommandBuilder.Append($" PRECEDING"); + } + + if(windowsFrameExpression.Following != null) + { + _relationalCommandBuilder.Append($" AND "); + + if (windowsFrameExpression.Following is SqlConstantExpression followingExpression && followingExpression?.Type == typeof(RowsFollowing)) + { + _relationalCommandBuilder.Append((RowsPreceding)followingExpression.Value! == RowsPreceding.CurrentRow + ? "CURRENT ROW" + : "UNBOUNDED FOLLOWING"); + } + else + { + Visit(windowsFrameExpression.Following); + + _relationalCommandBuilder.Append($" FOLLOWING"); + } + } + + if (windowsFrameExpression.Exclude is SqlConstantExpression excludeExpression && excludeExpression?.Type == typeof(FrameExclude)) + { + _relationalCommandBuilder.Append($" EXCLUDE "); + + _relationalCommandBuilder.Append((FrameExclude)excludeExpression.Value! switch + { + FrameExclude.NoOthers => "NO OTHERS", + FrameExclude.CurrentRow => "CURRENT ROW", + FrameExclude.Group => "GROUP", + FrameExclude.Ties => "TIES", + _ => throw new ArgumentOutOfRangeException() + }); + } + + return windowsFrameExpression; + } } diff --git a/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs b/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs index 91810a03cdb..73325e6b5a8 100644 --- a/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs +++ b/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs @@ -43,10 +43,11 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model return false; } - if (method.DeclaringType == typeof(RelationalDbFunctionsExtensions)) + if (method.DeclaringType == typeof(RelationalDbFunctionsExtensions) || method.DeclaringType == typeof(WindowFunctionsExtensions)) { return false; } + } return base.IsEvaluatableExpression(expression, model); diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 9d26798eade..2678575c672 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -896,10 +896,125 @@ bool TryFlattenVisit(Expression argument) ? translatedAggregate : TranslateAsSubquery(methodCallExpression); + // match windowing functions + case + { + Method.Name: nameof(WindowFunctionsExtensions.Over) + } when method.DeclaringType == typeof(WindowFunctionsExtensions): + { + return Dependencies.WindowBuilderExpressionFactory.CreateWindowBuilder(); + } + + //what can I put in this case? + case + { + + } when arguments.Count > 0 && typeof(IWindowFinal).IsAssignableFrom(arguments[0].Type): + { + //create object to deal with all of these else/if cases for windowing functions? + //this is the aggregate. Do we need something better than WindowFunctionsExtensions - how will custom providers add specific aggs + //todo - how many args could there be? might have to loop this. hardcode for max for now + + var aggregateParams = new SqlExpression[arguments.Count - 1]; + + for (var i = 1; i < arguments.Count; i++) + { + if (TranslationFailed(arguments[i], Visit(RemoveObjectConvert(arguments[i] is LambdaExpression lambda ? lambda.Body : arguments[i])), out var translatedValue)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + aggregateParams[i - 1] = translatedValue!; + } + + var windowingFunction = Dependencies.WindowAggregateMethodCallTranslator.Translate(method, aggregateParams, _queryCompilationContext.Logger) as SqlFunctionExpression; + + if (windowingFunction == null) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + var wbe = (RelationalWindowBuilderExpression)Visit(arguments[0]); + + return _sqlExpressionFactory.Over(windowingFunction, wbe.PartitionExpression, wbe.OrderingExpressions, wbe.FrameExpression); + } + + case + { + Method.Name: nameof(IOver.PartitionBy) + } when method.DeclaringType == typeof(IOver): + { + if (!(Visit(methodCallExpression.Object) is RelationalWindowBuilderExpression wbe)) + return QueryCompilationContext.NotTranslatedExpression; + + var partitions = (NewArrayExpression)arguments[0]; + var translatedPartitions = new SqlExpression[partitions.Expressions.Count]; + + for (var i = 0; i < partitions.Expressions.Count; i++) + { + if (TranslationFailed(partitions.Expressions[i], Visit(partitions.Expressions[i]), out var translatedValue)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + translatedPartitions[i] = translatedValue!; + } + + wbe.AddPartitionBy(translatedPartitions); + + return wbe!; + } + + //anything to put in this case? + case + { + + } when method.DeclaringType == typeof(IOrderRoot) + || method.DeclaringType == typeof(IOrderThen): + { + if (!(Visit(methodCallExpression.Object) is RelationalWindowBuilderExpression wbe)) + return QueryCompilationContext.NotTranslatedExpression; + + if (TranslationFailed(arguments[0], Visit(RemoveObjectConvert(arguments[0])), out var sqlOject)) + { + return QueryCompilationContext.NotTranslatedExpression; + } + + wbe.AddOrdering(sqlOject!, method.Name == nameof(IOrderRoot.OrderBy) + || method.Name == nameof(IOrderThen.ThenBy)); + + return wbe!; + } + + //another open case + case + { + + } when method.DeclaringType == typeof(IFrame): + { + if (!(Visit(methodCallExpression.Object) is RelationalWindowBuilderExpression wbe)) + return QueryCompilationContext.NotTranslatedExpression; + + var preceding = Visit(arguments[0]) as SqlConstantExpression; + + if (preceding == null) + return QueryCompilationContext.NotTranslatedExpression; + + var following = arguments.Count == 2 ? Visit(arguments[1]) as SqlConstantExpression : null; + + if (following == null && arguments.Count == 2) + return QueryCompilationContext.NotTranslatedExpression; + + //todo - should I key off the the string rows here? What about when someone has to override to add Groups? + wbe.AddFrame(method, preceding, following); + + return wbe; + } + default: { scalarArguments = []; - if (!TryTranslateAsEnumerableExpression(methodCallExpression.Object, out enumerableExpression) + if (!TryTranslateAsEnumerableExpression(methodCallExpression!.Object, out enumerableExpression) && TranslationFailed(methodCallExpression.Object, Visit(methodCallExpression.Object), out sqlObject)) { return TranslateAsSubquery(methodCallExpression); diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitorDependencies.cs index 144881f451c..ced3938ccbc 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitorDependencies.cs @@ -51,7 +51,9 @@ public RelationalSqlTranslatingExpressionVisitorDependencies( IRelationalTypeMappingSource typeMappingSource, IMemberTranslatorProvider memberTranslatorProvider, IMethodCallTranslatorProvider methodCallTranslatorProvider, - IAggregateMethodCallTranslatorProvider aggregateMethodCallTranslatorProvider) + IAggregateMethodCallTranslatorProvider aggregateMethodCallTranslatorProvider, + IWindowAggregateMethodCallTranslator windowAggregateMethodCallTranslator, + IWindowBuilderExpressionFactory windowBuilderExpressionFactory) { SqlExpressionFactory = sqlExpressionFactory; Model = model; @@ -59,6 +61,8 @@ public RelationalSqlTranslatingExpressionVisitorDependencies( MemberTranslatorProvider = memberTranslatorProvider; MethodCallTranslatorProvider = methodCallTranslatorProvider; AggregateMethodCallTranslatorProvider = aggregateMethodCallTranslatorProvider; + WindowBuilderExpressionFactory = windowBuilderExpressionFactory; + WindowAggregateMethodCallTranslator = windowAggregateMethodCallTranslator; } /// @@ -90,4 +94,14 @@ public RelationalSqlTranslatingExpressionVisitorDependencies( /// The aggregate method-call translation provider. /// public IAggregateMethodCallTranslatorProvider AggregateMethodCallTranslatorProvider { get; } + + /// + /// The window aggregate method-call translation provider. + /// + public IWindowAggregateMethodCallTranslator WindowAggregateMethodCallTranslator { get; } + + /// + /// todo + /// + public IWindowBuilderExpressionFactory WindowBuilderExpressionFactory { get; } } diff --git a/src/EFCore.Relational/Query/RelationalWindowAggregateFunctionExtensions.cs b/src/EFCore.Relational/Query/RelationalWindowAggregateFunctionExtensions.cs new file mode 100644 index 00000000000..c7daf601ba7 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalWindowAggregateFunctionExtensions.cs @@ -0,0 +1,332 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public static class RelationalWindowAggregateFunctionExtensions +{ + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Average(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Average(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static int? Count(this IWindowFinal final) + { + throw new Exception(); + } + + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static int? Count(this IWindowFinal final, Func filter) + { + throw new Exception(); + } + + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static int? Count(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static int? Count(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static double CumeDist(this IWindowFinal final) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static long DenseRank(this IOrderThen final) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource FirstValue(this IOrderThen final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource FirstValue(this IFrameResults final, TSource source) + { + //todo - how do we force this to include an order by? + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Lag(this IOrderThen final, TSource source, int offset, TSource defaultValue) + { + //todo - how do we force this to include an order by? + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource LastValue(this IOrderThen final, TSource source) + { + //todo - how do we force this to include an order by? + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource LastValue(this IFrameResults final, TSource source) + { + //todo - how do we force this to include an order by? + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Lead(this IOrderThen final, TSource source, int offset, TSource defaultValue) + { + //todo - how do we force this to include an order by? + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Max(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Max(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Min(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Min(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static long NTile(this IOrderThen final, int numberOfGroups) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static double PercentRank(this IOrderThen final) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static long Rank(this IOrderThen final) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static long RowNumber(this IOrderThen final) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Sum(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static TSource? Sum(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } +} diff --git a/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs b/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs new file mode 100644 index 00000000000..ce9207f5a01 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// 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 RelationalWindowAggregateMethodTranslator : IWindowAggregateMethodCallTranslator +{ + 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 RelationalWindowAggregateMethodTranslator(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, IReadOnlyList arguments, IDiagnosticsLogger logger) + { + var methodInfo = method.IsGenericMethod + ? method.GetGenericMethodDefinition() + : method; + + //todo find better way to make sure we are dealing with the correct method + //todo - dictionary instead of switch? + switch (methodInfo.Name) + { + case nameof(RelationalWindowAggregateFunctionExtensions.Average) + when methodInfo == RelationalWindowAggregateMethods.Average: + + return _sqlExpressionFactory.Function("AVG", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Average) + when methodInfo == RelationalWindowAggregateMethods.AverageFilter: + + return _sqlExpressionFactory.Function("AVG", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Count) + when methodInfo == RelationalWindowAggregateMethods.CountAll: + + return _sqlExpressionFactory.Function("COUNT", [_sqlExpressionFactory.Fragment("*")], false, [false], typeof(int)); + + case nameof(RelationalWindowAggregateFunctionExtensions.Count) + when methodInfo == RelationalWindowAggregateMethods.CountAllFilter: + + return _sqlExpressionFactory.Function("COUNT", BuildCaseExpression(arguments, _sqlExpressionFactory.Constant("1")), true, [false], typeof(int)); + + case nameof(RelationalWindowAggregateFunctionExtensions.Count) + when methodInfo == RelationalWindowAggregateMethods.CountCol: + + return _sqlExpressionFactory.Function("COUNT", arguments, false, [false], typeof(int)); + + case nameof(RelationalWindowAggregateFunctionExtensions.Count) + when methodInfo == RelationalWindowAggregateMethods.CountColFilter: + + return _sqlExpressionFactory.Function("COUNT", BuildCaseExpression(arguments), true, [false], typeof(int)); + + case nameof(RelationalWindowAggregateFunctionExtensions.CumeDist) + when methodInfo == RelationalWindowAggregateMethods.CumeDist: + + return _sqlExpressionFactory.Function("CUME_DIST", Enumerable.Empty(), false, [], typeof(double)); + + case nameof(RelationalWindowAggregateFunctionExtensions.DenseRank) + when methodInfo == RelationalWindowAggregateMethods.DenseRank: + + return _sqlExpressionFactory.Function("DENSE_RANK", Enumerable.Empty(), false, [], typeof(long)); + + case nameof(RelationalWindowAggregateFunctionExtensions.FirstValue) + when methodInfo == RelationalWindowAggregateMethods.FirstValueFrameResults: + + case nameof(RelationalWindowAggregateFunctionExtensions.FirstValue) + when methodInfo == RelationalWindowAggregateMethods.FirstValueOrderThen: + + return _sqlExpressionFactory.Function("FIRST_VALUE", arguments, true, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Lag) + when methodInfo == RelationalWindowAggregateMethods.Lag: + + return _sqlExpressionFactory.Function("LAG", arguments, true, [false, false, false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.LastValue) + when methodInfo == RelationalWindowAggregateMethods.LastValueOrderThen: + + case nameof(RelationalWindowAggregateFunctionExtensions.LastValue) + when methodInfo == RelationalWindowAggregateMethods.LastValueFrameResults: + + return _sqlExpressionFactory.Function("LAST_VALUE", arguments, true, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Lead) + when methodInfo == RelationalWindowAggregateMethods.Lead: + + return _sqlExpressionFactory.Function("LEAD", arguments, true, [false, false, false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Max) + when methodInfo == RelationalWindowAggregateMethods.Max: + + return _sqlExpressionFactory.Function("MAX", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Max) + when methodInfo == RelationalWindowAggregateMethods.MaxFilter: + + return _sqlExpressionFactory.Function("MAX", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Min) + when methodInfo == RelationalWindowAggregateMethods.Min: + + return _sqlExpressionFactory.Function("MIN", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Min) + when methodInfo == RelationalWindowAggregateMethods.MinFilter: + + return _sqlExpressionFactory.Function("MIN", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.NTile) + when methodInfo == RelationalWindowAggregateMethods.NTile: + + return _sqlExpressionFactory.Function("NTILE", arguments, false, [false], typeof(long)); + + case nameof(RelationalWindowAggregateFunctionExtensions.PercentRank) + when methodInfo == RelationalWindowAggregateMethods.PercentRank: + + return _sqlExpressionFactory.Function("PERCENT_RANK", Enumerable.Empty(), false, [], typeof(double)); + + case nameof(RelationalWindowAggregateFunctionExtensions.Rank) + when methodInfo == RelationalWindowAggregateMethods.Rank: + + return _sqlExpressionFactory.Function("RANK", Enumerable.Empty(), false, [], typeof(long)); + + case nameof(RelationalWindowAggregateFunctionExtensions.RowNumber) + when methodInfo == RelationalWindowAggregateMethods.RowNumber: + + return _sqlExpressionFactory.Function("ROW_NUMBER", Enumerable.Empty(), false, [], typeof(long)); + + case nameof(RelationalWindowAggregateFunctionExtensions.Sum) + when methodInfo == RelationalWindowAggregateMethods.Sum: + + return _sqlExpressionFactory.Function("SUM", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + + case nameof(RelationalWindowAggregateFunctionExtensions.Sum) + when methodInfo == RelationalWindowAggregateMethods.SumFilter: + + return _sqlExpressionFactory.Function("SUM", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + } + + return null; + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + protected virtual SqlExpression[] BuildCaseExpression(IReadOnlyList arguments, SqlExpression? result = null) + => [_sqlExpressionFactory.Case([new CaseWhenClause(ProcessCaseWhen(arguments[result == null ? 1 : 0]), result ?? arguments[0])], _sqlExpressionFactory.Constant(null, typeof(object)))] ; + + + /// + /// todo + /// + /// todo + /// todo + protected virtual SqlExpression ProcessCaseWhen(SqlExpression whenExpression) + { + if(whenExpression is SqlBinaryExpression { Left : InExpression inExpression, Right : SqlConstantExpression constantExpression }) + { + return constantExpression.Value as bool? == true + ? inExpression + : _sqlExpressionFactory.Not(inExpression); + } + + return whenExpression; + } +} diff --git a/src/EFCore.Relational/Query/RelationalWindowAggregateMethods.cs b/src/EFCore.Relational/Query/RelationalWindowAggregateMethods.cs new file mode 100644 index 00000000000..3f3036a468a --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalWindowAggregateMethods.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +internal static class RelationalWindowAggregateMethods +{ + static RelationalWindowAggregateMethods() + { + var aggMethods = typeof(RelationalWindowAggregateFunctionExtensions).GetMethods().Where(mi => typeof(IWindowFinal).IsAssignableFrom(mi.GetParameters().FirstOrDefault()?.ParameterType)).ToList(); + + Average = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Average) && m.GetParameters().Length == 2); + AverageFilter = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Average) && m.GetParameters().Length == 3); + CountAll = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Count) && m.GetParameters().Length == 1); + CountAllFilter = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Count) && m.GetParameters().Length == 2 && typeof(Func).IsAssignableFrom(m.GetParameters()[1].ParameterType)); + CountCol = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Count) && m.GetParameters().Length == 2 && !typeof(Func).IsAssignableFrom(m.GetParameters()[1].ParameterType)); + CountColFilter = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Count) && m.GetParameters().Length == 3); + CumeDist = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.CumeDist)); + DenseRank = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.DenseRank)); + FirstValueFrameResults = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.FirstValue) && typeof(IFrameResults).IsAssignableFrom(m.GetParameters().FirstOrDefault()?.ParameterType)); + FirstValueOrderThen = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.FirstValue) && typeof(IOrderThen).IsAssignableFrom(m.GetParameters().FirstOrDefault()?.ParameterType)); + Lag = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Lag)); + LastValueFrameResults = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.LastValue) && typeof(IFrameResults).IsAssignableFrom(m.GetParameters().FirstOrDefault()?.ParameterType)); + LastValueOrderThen = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.LastValue) && typeof(IOrderThen).IsAssignableFrom(m.GetParameters().FirstOrDefault()?.ParameterType)); + Lead = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Lead)); + Max = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Max) && m.GetParameters().Length == 2); + MaxFilter = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Max) && m.GetParameters().Length == 3); + Min = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Min) && m.GetParameters().Length == 2); + MinFilter = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Min) && m.GetParameters().Length == 3); + NTile = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.NTile)); + PercentRank = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.PercentRank)); + Rank = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Rank)); + RowNumber = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.RowNumber)); + Sum = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Sum) && m.GetParameters().Length == 2); + SumFilter = aggMethods.Single(m => m.Name == nameof(RelationalWindowAggregateFunctionExtensions.Sum) && m.GetParameters().Length == 3); + } + + public static MethodInfo Average { get; } + public static MethodInfo AverageFilter { get; } + public static MethodInfo CountAll { get; } + public static MethodInfo CountAllFilter { get; } + public static MethodInfo CountCol { get; } + public static MethodInfo CountColFilter { get; } + public static MethodInfo CumeDist { get; } + public static MethodInfo DenseRank { get; } + public static MethodInfo FirstValueFrameResults { get; } + public static MethodInfo FirstValueOrderThen { get; } + public static MethodInfo Lag { get; } + public static MethodInfo Lead { get; } + public static MethodInfo LastValueFrameResults { get; } + public static MethodInfo LastValueOrderThen { get; } + public static MethodInfo Max { get; } + public static MethodInfo MaxFilter { get; } + public static MethodInfo Min { get; } + public static MethodInfo MinFilter { get; } + public static MethodInfo NTile { get; } + public static MethodInfo PercentRank { get; } + public static MethodInfo Rank { get; } + public static MethodInfo RowNumber { get; } + public static MethodInfo Sum { get; } + public static MethodInfo SumFilter { get; } +} diff --git a/src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs b/src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs new file mode 100644 index 00000000000..48ea472f4bb --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public class RelationalWindowBuilderExpression : Expression +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + private readonly List _orderingExpressions = new List(); + private WindowPartitionExpression? _partitionExpression; + private WindowFrameExpression? _frameExpression; + private SqlConstantExpression? _excludeExpression; + private SqlExpression? _filterExpression; + + /// + /// todo + /// + /// todo + public RelationalWindowBuilderExpression(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// todo + /// + public IReadOnlyList OrderingExpressions => _orderingExpressions; + + /// + /// todo + /// + public WindowPartitionExpression? PartitionExpression => _partitionExpression; + + /// + /// todo + /// + public WindowFrameExpression? FrameExpression => _frameExpression; + + /// + /// todo + /// + public SqlConstantExpression? ExcludeExpression => _excludeExpression; + + /// + /// todo + /// + public virtual void AddOrdering(SqlExpression expression, bool ascending) => _orderingExpressions.Add(new OrderingExpression(expression, ascending)); + + /// + /// todo + /// + public virtual void AddPartitionBy(SqlExpression[] partitions) => _partitionExpression = _sqlExpressionFactory.PartitionBy(partitions); + + /// + /// todo + /// + public virtual void AddFrame(MethodInfo method, SqlExpression? preceding, SqlExpression? following) => _frameExpression = _sqlExpressionFactory.WindowFrame(method, preceding, following, _excludeExpression); + + /// + /// todo + /// + public virtual void AddFilter(SqlExpression filter) => _filterExpression = filter; + + /// + /// todo + /// + public virtual void AddExclude(SqlConstantExpression expression) + { + _excludeExpression = expression; + + if(_frameExpression != null) + { + _frameExpression.Exclude = _excludeExpression; + } + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index 976d024e282..48bf66066b5 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -1,7 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Query.Internal; +using System.Xml.Linq; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.Query; @@ -67,6 +71,7 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) SqlFunctionExpression e => e.ApplyTypeMapping(typeMapping), SqlParameterExpression e => e.ApplyTypeMapping(typeMapping), SqlUnaryExpression e => ApplyTypeMappingOnSqlUnary(e, typeMapping), + WindowOverExpression e => e.ApplyTypeMapping(typeMapping), _ => sqlExpression }; @@ -966,4 +971,98 @@ public virtual SqlExpression Constant(object value, RelationalTypeMapping? typeM /// public virtual SqlExpression Constant(object? value, Type type, RelationalTypeMapping? typeMapping = null) => new SqlConstantExpression(value, type, typeMapping); + + /// + public virtual bool TryCreateLeast( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? leastExpression) + { + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + + expressions = FlattenLeastGreatest("LEAST", expressions); + + leastExpression = Function( + "LEAST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } + + /// + public virtual bool TryCreateGreatest( + IReadOnlyList expressions, + Type resultType, + [NotNullWhen(true)] out SqlExpression? greatestExpression) + { + var resultTypeMapping = ExpressionExtensions.InferTypeMapping(expressions); + + expressions = FlattenLeastGreatest("GREATEST", expressions); + + greatestExpression = Function( + "GREATEST", expressions, nullable: true, Enumerable.Repeat(true, expressions.Count), resultType, resultTypeMapping); + return true; + } + + /// + public virtual WindowPartitionExpression PartitionBy(IEnumerable paritions) + { + var typeMappedArguments = new List(); + + foreach (var partition in paritions) + { + typeMappedArguments.Add(ApplyDefaultTypeMapping(partition)); + } + + return new WindowPartitionExpression(typeMappedArguments); + } + + /// + public virtual WindowFrameExpression WindowFrame(MethodInfo method, SqlExpression? preceding, SqlExpression? following, SqlExpression? exclude) + { + if (string.Compare(method.Name, "rows", StringComparison.OrdinalIgnoreCase) == 0) + return new WindowFrameRowExpression(ApplyDefaultTypeMapping(preceding), ApplyDefaultTypeMapping(following), ApplyDefaultTypeMapping(exclude)); + else if (string.Compare(method.Name, "range", StringComparison.OrdinalIgnoreCase) == 0) + return new WindowFrameRangeExpression(ApplyDefaultTypeMapping(preceding), ApplyDefaultTypeMapping(following), ApplyDefaultTypeMapping(exclude)); + else if (string.Compare(method.Name, "groups", StringComparison.OrdinalIgnoreCase) == 0) + return new WindowFrameGroupsExpression(ApplyDefaultTypeMapping(preceding), ApplyDefaultTypeMapping(following), ApplyDefaultTypeMapping(exclude)); + else + throw new Exception($"Unsupported Frame Method {method.Name}"); + } + + /// + public virtual WindowOverExpression Over(SqlFunctionExpression aggregate, WindowPartitionExpression? partition, IReadOnlyList orderings, + WindowFrameExpression? frame) + { + return new WindowOverExpression(aggregate, partition, orderings, frame); + } + + private IReadOnlyList FlattenLeastGreatest(string functionName, IReadOnlyList expressions) + { + List? flattenedExpressions = null; + + for (var i = 0; i < expressions.Count; i++) + { + var expression = expressions[i]; + if (expression is SqlFunctionExpression { IsBuiltIn: true } nestedFunction + && nestedFunction.Name == functionName) + { + if (flattenedExpressions is null) + { + flattenedExpressions = []; + for (var j = 0; j < i; j++) + { + flattenedExpressions.Add(expressions[j]); + } + } + + Check.DebugAssert(nestedFunction.Arguments is not null, "Null arguments to " + functionName); + flattenedExpressions.AddRange(nestedFunction.Arguments); + } + else + { + flattenedExpressions?.Add(expressions[i]); + } + } + + return flattenedExpressions ?? expressions; + } } diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index bb4c44c9307..8697b371392 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -57,6 +57,9 @@ ShapedQueryExpression shapedQueryExpression UpdateExpression updateExpression => VisitUpdate(updateExpression), JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression), ValuesExpression valuesExpression => VisitValues(valuesExpression), + WindowOverExpression overExpression => VisitOver(overExpression), + WindowPartitionExpression partitionExpression => VisitWindowPartition(partitionExpression), + WindowFrameExpression windowFrameExpression => VisitWindowFrame(windowFrameExpression), _ => base.VisitExtension(extensionExpression), }; @@ -304,4 +307,25 @@ ShapedQueryExpression shapedQueryExpression /// The expression to visit. /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. protected abstract Expression VisitValues(ValuesExpression valuesExpression); + + /// + /// todo + /// + /// todo + /// todo + protected abstract Expression VisitOver(WindowOverExpression windowOverExpression); + + /// + /// todo + /// + /// todo + /// todo + protected abstract Expression VisitWindowFrame(WindowFrameExpression windowFrameExpression); + + /// + /// todo + /// + /// todo + /// todo + protected abstract Expression VisitWindowPartition(WindowPartitionExpression windowPartitionExpression); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs new file mode 100644 index 00000000000..1843e53dc6d --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// todo +/// +public abstract class WindowFrameExpression : Expression, IPrintableExpression +{ + /// + /// todo + /// + public SqlExpression? Preceding { get; init; } + + /// + /// todo + /// + public SqlExpression? Following { get; init; } + + /// + /// todo - bettter name + /// + public abstract string FrameName { get; } + + /// + /// todo + /// + public SqlExpression? Exclude { get; set; } + + /// + /// todo + /// + /// todo + /// todo + /// todo + public WindowFrameExpression(SqlExpression? preceding, SqlExpression? following, SqlExpression? exclude) + { + //todo - exception if both preceding and follow are null? + + Preceding = preceding; + Following = following; + Exclude = exclude; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + /// + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + { + //todo + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is WindowFrameExpression windowFrameExpression + && Equals(windowFrameExpression)); + + private bool Equals(WindowFrameExpression windowFrameExpression) + => base.Equals(windowFrameExpression) + && FrameName == windowFrameExpression.FrameName + && ((Preceding == null && windowFrameExpression.Preceding == null) + || (Preceding != null && Preceding.Equals(windowFrameExpression.Preceding))) + && ((Following == null && windowFrameExpression.Following == null) + || (Following != null && Following.Equals(windowFrameExpression.Following))) + && ((Exclude == null && windowFrameExpression.Exclude == null) + || (Exclude != null && Exclude.Equals(windowFrameExpression.Exclude))); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + hash.Add(FrameName); + hash.Add(Preceding); + hash.Add(Following); + hash.Add(Exclude); + + return hash.ToHashCode(); + } +} + +/// +/// todo +/// +public class WindowFrameRowExpression : WindowFrameExpression +{ + /// + public override string FrameName => "ROWS"; + + /// + /// todo + /// + /// todo + /// todo + /// todo + public WindowFrameRowExpression(SqlExpression? preceding, SqlExpression? following, SqlExpression? exclude) + : base(preceding, following, exclude) + { + } +} + +/// +/// todo +/// +public class WindowFrameRangeExpression : WindowFrameExpression +{ + /// + public override string FrameName => "RANGE"; + + /// + /// todo + /// + /// todo + /// todo + /// todo + public WindowFrameRangeExpression(SqlExpression? preceding, SqlExpression? following, SqlExpression? exclude) + : base(preceding, following, exclude) + { + } +} + +/// +/// todo +/// +public class WindowFrameGroupsExpression : WindowFrameExpression +{ + /// + public override string FrameName => "GROUPS"; + + /// + /// todo + /// + /// todo + /// todo + /// todo + public WindowFrameGroupsExpression(SqlExpression? preceding, SqlExpression? following, SqlExpression? exclude) + : base(preceding, following, exclude) + { + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs new file mode 100644 index 00000000000..e9858866173 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// test +/// +public class WindowOverExpression : SqlExpression, IPrintableExpression +{ + /// + /// todo + /// + public WindowPartitionExpression? Partition { get; init; } + + /// + /// todo + /// + public SqlFunctionExpression Aggregate { get; set; } + + /// + /// todo + /// + public IReadOnlyList Ordering { get; init; } + + /// + /// todo + /// + public WindowFrameExpression? WindowFrame { get; init; } + + /// + /// todo + /// + public SqlExpression? Filter { get; init; } + + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public WindowOverExpression(SqlFunctionExpression aggregateExpression, WindowPartitionExpression? partitionExpression, + IReadOnlyList orderingExpressions, WindowFrameExpression? windowframeExpression) + : base(aggregateExpression.Type, aggregateExpression.TypeMapping) + { + Partition = partitionExpression; + Aggregate = aggregateExpression; + Ordering = orderingExpressions; + WindowFrame = windowframeExpression; + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public WindowOverExpression(SqlFunctionExpression aggregateExpression, WindowPartitionExpression? partitionExpression, + IReadOnlyList orderingExpressions, WindowFrameExpression? windowframeExpression, SqlExpression? filterExpression, + Type type, RelationalTypeMapping? relationalTypeMapping) + : base(type, relationalTypeMapping) + { + Partition = partitionExpression; + Aggregate = aggregateExpression; + Ordering = orderingExpressions; + WindowFrame = windowframeExpression; + Filter = filterExpression; + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var aggregate = (SqlFunctionExpression)visitor.Visit(Aggregate); + var partition = Partition != null ? visitor.Visit(Partition) as WindowPartitionExpression : null; + var orderBys = new List(); + var frame = visitor.Visit(WindowFrame) as WindowFrameExpression; + + foreach (var orderingExpression in Ordering) + { + var newOrder = (OrderingExpression)visitor.Visit(orderingExpression); + orderBys.Add(newOrder); + } + + return Update(partition, aggregate, orderBys, frame); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public virtual WindowOverExpression Update( + WindowPartitionExpression? partition, + SqlFunctionExpression aggregate, + IReadOnlyList ordering, + WindowFrameExpression? frame) + => partition != Partition || aggregate != Aggregate || frame != WindowFrame || !Enumerable.SequenceEqual(ordering, Ordering) + ? new WindowOverExpression(aggregate, partition, ordering, frame) + : this; + + + /// + /// Applies supplied type mapping to this expression. + /// + /// A relational type mapping to apply. + /// A new expression which has supplied type mapping. + public virtual WindowOverExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) + => new( + Aggregate.ApplyTypeMapping(typeMapping), + Partition, + Ordering, + WindowFrame, + Filter, + Type, + typeMapping ?? TypeMapping); + + /// + /// todo + /// + /// todo + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("OVER "); + } + + /// + /// todo + /// + /// todo + public override Expression Quote() + { + //what is this supposed to do? + return this; + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is WindowOverExpression windowOverExpression + && Equals(windowOverExpression)); + + private bool Equals(WindowOverExpression windowOverExpression) + => base.Equals(windowOverExpression) + && Aggregate.Equals(windowOverExpression.Aggregate) + && ((Partition == null && windowOverExpression.Partition == null) + || (Partition != null && Partition.Equals(windowOverExpression.Partition))) + && ((WindowFrame == null && windowOverExpression.WindowFrame == null) + || (WindowFrame != null && WindowFrame.Equals(windowOverExpression.WindowFrame))) + & ((Filter == null && windowOverExpression.Filter == null) + || (Filter != null && Filter.Equals(windowOverExpression.Filter))) + && ((Ordering == null && windowOverExpression.Ordering == null) + || (Ordering != null && windowOverExpression.Ordering != null + && Ordering.SequenceEqual(windowOverExpression.Ordering))); + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + hash.Add(Aggregate); + hash.Add(WindowFrame); + hash.Add(WindowFrame); + + if (Ordering != null) + { + for (var i = 0; i < Ordering.Count; i++) + { + hash.Add(Ordering[i]); + } + } + + return hash.ToHashCode(); + } +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs new file mode 100644 index 00000000000..b2c785f17f4 --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// test +/// +public class WindowPartitionExpression : Expression, IPrintableExpression +{ + /// + /// todo + /// + public IReadOnlyList Partitions { get; init; } + + /// + /// tests + /// + /// test + public WindowPartitionExpression(IReadOnlyList partitions) + { + Partitions = partitions; + } + + /// + /// todo + /// + /// todo + public void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append("PARTITION BY "); + } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var newParts = new List(); + + bool changed = false; + + foreach(var partition in Partitions) + { + var newPart = (SqlExpression)visitor.Visit(partition); + + newParts.Add(newPart); + + changed |= partition != newPart; + } + + return changed + ? new WindowPartitionExpression(newParts) + : this; + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is WindowPartitionExpression windowPartitionExpression + && Equals(windowPartitionExpression)); + + + private bool Equals(WindowPartitionExpression windowPartitionExpression) + => base.Equals(windowPartitionExpression) + && ((Partitions == null && windowPartitionExpression.Partitions == null) + || (Partitions != null && windowPartitionExpression.Partitions != null + && Partitions.SequenceEqual(windowPartitionExpression.Partitions))); + + + /// + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(base.GetHashCode()); + + if (Partitions != null) + { + for (var i = 0; i < Partitions.Count; i++) + { + hash.Add(Partitions[i]); + } + } + + return hash.ToHashCode(); + } +} + diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index a05a69773f8..b9850d31ec6 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -280,6 +280,9 @@ SqlUnaryExpression sqlUnaryExpression => VisitSqlUnary(sqlUnaryExpression, allowOptimizedExpansion, out nullable), JsonScalarExpression jsonScalarExpression => VisitJsonScalar(jsonScalarExpression, allowOptimizedExpansion, out nullable), + WindowOverExpression windowOverExpression + => VisitWindowOverExpression(windowOverExpression, allowOptimizedExpansion, out nullable), + _ => VisitCustomSqlExpression(sqlExpression, allowOptimizedExpansion, out nullable) }; @@ -1399,6 +1402,23 @@ protected virtual SqlExpression VisitJsonScalar( return jsonScalarExpression; } + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + protected virtual SqlExpression VisitWindowOverExpression( + WindowOverExpression windowOverExpression, + bool allowOptimizedExpansion, + out bool nullable) + { + nullable = windowOverExpression.Aggregate.IsNullable; + + return windowOverExpression; + } + /// /// Determines whether an will be transformed to an when it would /// otherwise require complex compensation for null semantics. diff --git a/src/EFCore.Relational/Query/WindowBuilderExpressionFactory.cs b/src/EFCore.Relational/Query/WindowBuilderExpressionFactory.cs new file mode 100644 index 00000000000..39f37479f09 --- /dev/null +++ b/src/EFCore.Relational/Query/WindowBuilderExpressionFactory.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public class WindowBuilderExpressionFactory : IWindowBuilderExpressionFactory +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// todo + /// + /// todo + public WindowBuilderExpressionFactory(ISqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// todo + /// + /// todo + public RelationalWindowBuilderExpression CreateWindowBuilder() + { + return new RelationalWindowBuilderExpression(_sqlExpressionFactory); + } +} diff --git a/src/EFCore.Relational/Query/WindowFunctionInterfaces.cs b/src/EFCore.Relational/Query/WindowFunctionInterfaces.cs new file mode 100644 index 00000000000..8b2707ac3ce --- /dev/null +++ b/src/EFCore.Relational/Query/WindowFunctionInterfaces.cs @@ -0,0 +1,198 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public enum RowsPreceding +{ + /// + /// todo + /// + CurrentRow, + + /// + /// todo + /// + UnboundedPreceding +} + +/// +/// todo +/// +public enum RowsFollowing +{ + /// + /// todo + /// + CurrentRow, + + /// + /// todo + /// + UnboundedFollowing +} + +/// +/// todo +/// +public enum FrameExclude +{ + /// + /// todo + /// + NoOthers, + + /// + /// todo + /// + CurrentRow, + + /// + /// todo + /// + Group, + + /// + /// todo + /// + Ties +} + +/// +/// todo +/// +public interface IOver : IOrderRoot, IWindowFinal +{ + /// + /// todo + /// + /// todo + IPartition PartitionBy(params object[] partitions); +} + +/// +/// todo +/// +public interface IPartition : IOrderRoot, IWindowFinal +{ +} + +/// +/// todo +/// +public interface IOrderRoot +{ + /// + /// todo + /// + /// todo + IOrderThen OrderBy(object orderBy); + + /// + /// todo + /// + /// todo + /// todo + IOrderThen OrderByDescending(object orderBy); +} + +/// +/// todo +/// +public interface IOrderThen : IFrame, IWindowFinal +{ + /// + /// todo + /// + /// todo + /// todo + IOrderThen ThenBy(object orderBy); + + /// + /// todo + /// + /// todo + /// todo + IOrderThen ThenByDescending(object orderBy); +} + +/// +/// todo +/// +public interface IFrame +{ + /// + /// todo + /// + /// todo + IFrameResults Rows(int preceding); + + /// + /// todo + /// + /// todo + IFrameResults Rows(RowsPreceding preceding); + + /// + /// todo + /// + /// todo + /// todo + IFrameResults Rows(int preceding, int following); + + /// + /// todo + /// + /// todo + /// todo + IFrameResults Rows(RowsPreceding preceding, int following); + + /// + /// todo + /// + /// todo + /// todo + IFrameResults Rows(int preceding, RowsFollowing following); + + /// + /// todo + /// + /// todo + /// todo + IFrameResults Rows(RowsPreceding preceding, RowsFollowing following); + + /// + /// todo + /// + /// todo + IFrameResults Range(RowsPreceding preceding); + + /// + /// todo + /// + /// todo + /// todo + IFrameResults Range(RowsPreceding preceding, RowsFollowing following); +} + +/// +/// todo +/// +public interface IFrameResults : IWindowFinal +{ } + +/// +/// todo +/// +public interface IWindowFinal +{ +} diff --git a/src/EFCore.Relational/Query/WindowFunctionsExtensions.cs b/src/EFCore.Relational/Query/WindowFunctionsExtensions.cs new file mode 100644 index 00000000000..7fb7685c64c --- /dev/null +++ b/src/EFCore.Relational/Query/WindowFunctionsExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public static class WindowFunctionsExtensions +{ + /// + /// todo + /// + /// todo + /// todo + public static IOver Over(this DbFunctions _) + { + throw new Exception(); + } +} diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 7e8c0696546..cfda19068b7 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -269,6 +269,8 @@ private static IServiceCollection AddEntityFrameworkSqlEngine(IServiceCollection .TryAdd() .TryAdd() .TryAdd() + //.TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateFunctionExtensions.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateFunctionExtensions.cs new file mode 100644 index 00000000000..57828df19be --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateFunctionExtensions.cs @@ -0,0 +1,176 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query; + +/// +/// todo +/// +public static class SqlServerWindowAggregateFunctionExtensions +{ + + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static long CountBig(this IWindowFinal final) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static long? CountBig(this IWindowFinal final, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static long CountBig(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static long? CountBig(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? Stdev(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? Stdev(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? StdevP(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? StdevP(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? Var(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? Var(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? VarP(this IWindowFinal final, TSource source) + { + throw new Exception(); + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + /// todo + /// todo + public static double? VarP(this IWindowFinal final, TSource source, Func filter) + { + throw new Exception(); + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs new file mode 100644 index 00000000000..92c3ee2da61 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs @@ -0,0 +1,114 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// todo +/// +public class SqlServerWindowAggregateMethodTranslator : RelationalWindowAggregateMethodTranslator +{ + private readonly ISqlExpressionFactory _sqlExpressionFactory; + + /// + /// todo + /// + /// todo + public SqlServerWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + : base(sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public override SqlExpression? Translate(MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger) + { + var translation = base.Translate(method, arguments, logger); + + if (translation != null) + return translation; + + var methodInfo = method.IsGenericMethod + ? method.GetGenericMethodDefinition() + : method; + + switch (methodInfo.Name) + { + case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) + when methodInfo == SqlServerWindowAggregateMethods.CountBigAll: + + return _sqlExpressionFactory.Function("COUNT_BIG", new[] { _sqlExpressionFactory.Fragment("*") }, false, [false], typeof(long)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) + when methodInfo == SqlServerWindowAggregateMethods.CountBigAllFilter: + + return _sqlExpressionFactory.Function("COUNT_BIG", BuildCaseExpression(arguments, _sqlExpressionFactory.Constant("1")), true, [false], typeof(long)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) + when methodInfo == SqlServerWindowAggregateMethods.CountBigCol: + + return _sqlExpressionFactory.Function("COUNT_BIG", arguments, false, [false], typeof(long)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) + when methodInfo == SqlServerWindowAggregateMethods.CountBigColFilter: + + return _sqlExpressionFactory.Function("COUNT_BIG", BuildCaseExpression(arguments), true, [false], typeof(long)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.Stdev) + when methodInfo == SqlServerWindowAggregateMethods.Stdev: + + return _sqlExpressionFactory.Function("STDEV", arguments, true, [false], typeof(double)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.Stdev) + when methodInfo == SqlServerWindowAggregateMethods.StdevFilter: + + return _sqlExpressionFactory.Function("STDEV", BuildCaseExpression(arguments), true, [false], typeof(double)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.StdevP) + when methodInfo == SqlServerWindowAggregateMethods.StdevP: + + return _sqlExpressionFactory.Function("STDEVP", arguments, true, [false], typeof(double)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.StdevP) + when methodInfo == SqlServerWindowAggregateMethods.StdevPFilter: + + return _sqlExpressionFactory.Function("STDEVP", BuildCaseExpression(arguments), true, [false], typeof(double)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.Var) + when methodInfo == SqlServerWindowAggregateMethods.Var: + + return _sqlExpressionFactory.Function("VAR", arguments, true, [false], typeof(double)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.Var) + when methodInfo == SqlServerWindowAggregateMethods.VarFilter: + + return _sqlExpressionFactory.Function("VAR", BuildCaseExpression(arguments), true, [false], typeof(double)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.VarP) + when methodInfo == SqlServerWindowAggregateMethods.VarP: + + return _sqlExpressionFactory.Function("VARP", arguments, true, [false], typeof(double)); + + case nameof(SqlServerWindowAggregateFunctionExtensions.VarP) + when methodInfo == SqlServerWindowAggregateMethods.VarPFilter: + + return _sqlExpressionFactory.Function("VARP", BuildCaseExpression(arguments), true, [false], typeof(double)); + } + + return null; + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethods.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethods.cs new file mode 100644 index 00000000000..13b8b627779 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethods.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +internal class SqlServerWindowAggregateMethods +{ + static SqlServerWindowAggregateMethods() + { + var aggMethods = typeof(SqlServerWindowAggregateFunctionExtensions).GetMethods().Where(mi => typeof(IWindowFinal).IsAssignableFrom(mi.GetParameters().FirstOrDefault()?.ParameterType)).ToList(); + + CountBigAll = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) && m.GetParameters().Length == 1); + CountBigAllFilter = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) && m.GetParameters().Length == 2 && typeof(Func).IsAssignableFrom(m.GetParameters()[1].ParameterType)); + + CountBigCol = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) && m.GetParameters().Length == 2 && !typeof(Func).IsAssignableFrom(m.GetParameters()[1].ParameterType)); + CountBigColFilter = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) && m.GetParameters().Length == 3); + + Stdev = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.Stdev) && m.GetParameters().Length == 2); + StdevFilter = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.Stdev) && m.GetParameters().Length == 3); + + StdevP = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.StdevP) && m.GetParameters().Length == 2); + StdevPFilter = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.StdevP) && m.GetParameters().Length == 3); + + Var = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.Var) && m.GetParameters().Length == 2); + VarFilter = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.Var) && m.GetParameters().Length == 3); + + VarP = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.VarP) && m.GetParameters().Length == 2); + VarPFilter = aggMethods.Single(m => m.Name == nameof(SqlServerWindowAggregateFunctionExtensions.VarP) && m.GetParameters().Length == 3); + } + + public static MethodInfo CountBigAll { get; } + public static MethodInfo CountBigAllFilter { get; } + + public static MethodInfo CountBigCol { get; } + public static MethodInfo CountBigColFilter { get; } + + public static MethodInfo Stdev { get; } + public static MethodInfo StdevFilter { get; } + + public static MethodInfo StdevP { get; } + public static MethodInfo StdevPFilter { get; } + + public static MethodInfo Var { get; } + public static MethodInfo VarFilter { get; } + + public static MethodInfo VarP { get; } + public static MethodInfo VarPFilter { get; } +} diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs index 48aa49e1c2c..33c00232610 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteSqlTranslatingExpressionVisitor.cs @@ -28,6 +28,10 @@ private static readonly MethodInfo StringEndsWithMethodInfo private static readonly MethodInfo EscapeLikePatternParameterMethod = typeof(SqliteSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!; + private static readonly string RangeExtension = nameof(SqliteWindowFunctionExtensions.Range); + private static readonly string GroupsExtension = nameof(SqliteWindowFunctionExtensions.Groups); + private static readonly string ExcludeExtension = nameof(SqliteWindowFunctionExtensions.Exclude); + private const char LikeEscapeChar = '\\'; private const string LikeEscapeString = "\\"; @@ -261,13 +265,46 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp { return translation1; } - - if (method == StringEndsWithMethodInfo + else if (method == StringEndsWithMethodInfo && TryTranslateStartsEndsWith( methodCallExpression.Object!, methodCallExpression.Arguments[0], startsWith: false, out var translation2)) { return translation2; } + else if((string.Compare(method.Name, RangeExtension, StringComparison.OrdinalIgnoreCase) == 0 && typeof(IWindowFinal).IsAssignableFrom(methodCallExpression.Arguments[0].Type)) + || string.Compare(method.Name, GroupsExtension, StringComparison.OrdinalIgnoreCase) == 0) + { + if (!(Visit(methodCallExpression.Arguments[0]) is RelationalWindowBuilderExpression wbe)) + return QueryCompilationContext.NotTranslatedExpression; + + var preceding = Visit(methodCallExpression.Arguments[1]) as SqlConstantExpression; + + if (preceding == null) + return QueryCompilationContext.NotTranslatedExpression; + + var following = methodCallExpression.Arguments.Count == 3 ? Visit(methodCallExpression.Arguments[2]) as SqlConstantExpression : null; + + if (following == null && methodCallExpression.Arguments.Count == 3) + return QueryCompilationContext.NotTranslatedExpression; + + wbe.AddFrame(method, preceding, following); + + return wbe; + } + else if(string.Compare(method.Name, ExcludeExtension, StringComparison.OrdinalIgnoreCase) == 0) + { + if (!(Visit(methodCallExpression.Arguments[0]) is RelationalWindowBuilderExpression wbe)) + return QueryCompilationContext.NotTranslatedExpression; + + var exclude = Visit(methodCallExpression.Arguments[1]) as SqlConstantExpression; + + if (exclude == null) + return QueryCompilationContext.NotTranslatedExpression; + + wbe.AddExclude(exclude); + + return wbe; + } return base.VisitMethodCall(methodCallExpression); diff --git a/src/EFCore.Sqlite.Core/Query/SqliteWindowFunctionExtensions.cs b/src/EFCore.Sqlite.Core/Query/SqliteWindowFunctionExtensions.cs new file mode 100644 index 00000000000..5a053c9f16a --- /dev/null +++ b/src/EFCore.Sqlite.Core/Query/SqliteWindowFunctionExtensions.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Query; + +/// +/// todo +/// +public static class SqliteWindowFunctionExtensions +{ + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IFrameResults Range(this IFrame frame, int preceding, int following) + => throw new NotImplementedException(); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IFrameResults Range(this IFrame frame, RowsPreceding preceding, int following) + => throw new NotImplementedException(); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IFrameResults Range(this IFrame frame, int preceding, RowsFollowing following) + => throw new NotImplementedException(); + + + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static IFrameResults Groups(this IFrame frame, int preceding) + => throw new NotImplementedException(); + + /// + /// todo + /// + /// todo + /// todo + /// todo + public static IFrameResults Groups(this IFrame frame, RowsPreceding preceding) + => throw new NotImplementedException(); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IFrameResults Groups(this IFrame frame, int preceding, int following) + => throw new NotImplementedException(); + + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IFrameResults Groups(this IFrame frame, RowsPreceding preceding, int following) + => throw new NotImplementedException(); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IFrameResults Groups(this IFrame frame, int preceding, RowsFollowing following) + => throw new NotImplementedException(); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IFrameResults Groups(this IFrame frame, RowsPreceding preceding, RowsFollowing following) + => throw new NotImplementedException(); + + /// + /// todo + /// + /// todo + /// todo + /// todo + /// todo + public static IWindowFinal Exclude(this IFrameResults frame, FrameExclude frameExclude) + => throw new NotImplementedException(); +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/WindowFunctionTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/WindowFunctionTestBase.cs new file mode 100644 index 00000000000..9d788c42a05 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/WindowFunctionTestBase.cs @@ -0,0 +1,1562 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class WindowFunctionTestBase : IClassFixture + where TFixture : SharedStoreFixtureBase, new() +{ + protected TFixture Fixture { get; } + + protected WindowFunctionTestBase(TFixture fixture) + { + Fixture = fixture; + } + + protected WindowFunctionContext CreateContext() + => (WindowFunctionContext)Fixture.CreateContext(); + + #region Model + + public class Employee + { + public int Id { get; set; } + public int EmployeeId { get; set; } + public string Name { get; set; } = ""; + public string DepartmentName { get; set; } = ""; + public decimal Salary { get; set; } + public int WorkExperience { get; set; } + } + + public class NullTestEmployee + { + public int Id { get; set; } + public int EmployeeId { get; set; } + public string Name { get; set; } = ""; + public string DepartmentName { get; set; } = ""; + public decimal? Salary { get; set; } + public int WorkExperience { get; set; } + } + + public class WindowFunctionContext : PoolableDbContext + { + public DbSet Employees => Set(); + public DbSet NullTestEmployees => Set(); + + public WindowFunctionContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(e => + { + e.Property(e => e.Salary).HasColumnType("decimal(10,2)"); + e.Property(e => e.Name).HasMaxLength(50); + e.Property(e => e.DepartmentName).HasMaxLength(25); + }); + + modelBuilder.Entity(e => + { + e.Property(e => e.Salary).HasColumnType("decimal(10,2)"); + e.Property(e => e.Name).HasMaxLength(50); + e.Property(e => e.DepartmentName).HasMaxLength(25); + }); + } + } + + public abstract class WindowFunctionFixture : SharedStoreFixtureBase + { + protected override Type ContextType { get; } = typeof(WindowFunctionContext); + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override bool ShouldLogCategory(string logCategory) + => logCategory == DbLoggerCategory.Query.Name; + + protected async override Task SeedAsync(DbContext context) + { + var ctx = context as WindowFunctionContext; + + context.Database.EnsureCreatedResiliently(); + + var emp1 = new Employee + { + EmployeeId = 1, + Name = "Luke Sykwalker", + Salary = 100000.0m, + WorkExperience = 10, + DepartmentName = "IT" + }; + + var emp2 = new Employee + { + EmployeeId = 2, + Name = "Darth Vader", + Salary = 500000.0m, + WorkExperience = 15, + DepartmentName = "Security" + }; + + var emp3 = new Employee + { + EmployeeId = 3, + Name = "Emperor Palpatine", + Salary = 1000000.53m, + WorkExperience = 20, + DepartmentName = "Corporate" + }; + + var emp4 = new Employee + { + EmployeeId = 4, + Name = "Leia Organa", + Salary = 200000.0m, + WorkExperience = 12, + DepartmentName = "IT" + }; + + var emp5 = new Employee + { + EmployeeId = 5, + Name = "Boba Fett", + Salary = 50000.0m, + WorkExperience = 4, + DepartmentName = "Security" + }; + + var emp6 = new Employee + { + EmployeeId = 6, + Name = "Han Solo", + Salary = 350000.24m, + WorkExperience = 8, + DepartmentName = "Sales" + }; + + var emp7 = new Employee + { + EmployeeId = 7, + Name = "Jabba the Hutt", + Salary = 1750000.0m, + WorkExperience = 18, + DepartmentName = "Sales" + }; + + var emp8 = new Employee + { + EmployeeId = 5, + Name = "Commander Cody", + Salary = 25000.12m, + WorkExperience = 4, + DepartmentName = "Security" + }; + + ctx!.Employees.AddRange(emp1, emp2, emp3, emp4, emp5, emp6, emp7, emp8); + + var nullEmp1 = new NullTestEmployee + { + EmployeeId = 1, + Name = "Battle Droid", + Salary = null, + WorkExperience = 1, + DepartmentName = "Security" + }; + + var nullEmp2 = new NullTestEmployee + { + EmployeeId = 2, + Name = "Super Battle Droid", + Salary = null, + WorkExperience = 2, + DepartmentName = "Security" + }; + + ctx.NullTestEmployees.AddRange(nullEmp1, nullEmp2); + + await context.SaveChangesAsync(); + } + } + + #endregion + + //todo - do tests need order by in the main query for comparing results? Is the order right now dependent on what the db gives us? + #region Tests + + #region Window Functions + + #region Max Tests + + [ConditionalFact] + public virtual void Max_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1750000.0m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Max_Parition_Order_Rows() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.CurrentRow, 5).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Max_Null() + { + using var context = CreateContext(); + + var results = context.NullTestEmployees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().Max(e.Salary) + }).ToList(); + + Assert.Equal(2, results.Count); + Assert.Null(results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Max_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Max(e.Salary, () => e.Salary > 100000) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + Assert.Equal(200000.00m, results[1].MaxSalary); + Assert.Equal(200000.00m, results[2].MaxSalary); + Assert.Equal(350000.24m, results[3].MaxSalary); + Assert.Equal(1750000.00m, results[4].MaxSalary); + Assert.Null(results[5].MaxSalary); + Assert.Null(results[6].MaxSalary); + Assert.Equal(500000.00m, results[7].MaxSalary); + } + + #endregion + + #region Min Tests + + [ConditionalFact] + public virtual void Min_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MinSalary = EF.Functions.Over().Min(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(25000.12m, results[0].MinSalary); + } + + [ConditionalFact] + public virtual void Min_Null() + { + using var context = CreateContext(); + + var results = context.NullTestEmployees.Select(e => new + { + e.Id, + e.Name, + MinSalary = EF.Functions.Over().Min(e.Salary) + }).ToList(); + + Assert.Equal(2, results.Count); + Assert.Null(results[0].MinSalary); + } + + [ConditionalFact] + public virtual void Min_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MinSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Min(e.Salary, () => e.Salary == 200000.00m) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Null(results[0].MinSalary); + Assert.Equal(200000.00m, results[1].MinSalary); + Assert.Equal(200000.00m, results[2].MinSalary); + Assert.Null(results[3].MinSalary); + Assert.Null(results[4].MinSalary); + Assert.Null(results[5].MinSalary); + Assert.Null(results[6].MinSalary); + Assert.Null(results[7].MinSalary); + } + + #endregion + + #region Count Tests + + [ConditionalFact] + public virtual void Count_Star_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().Count() + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(8, results[0].Count); + } + + [ConditionalFact] + public virtual void Count_Col_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().Count(e.Id) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(8, results[0].Count); + } + + [ConditionalFact] + public virtual void Count_Star_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Count(() => e.Salary <= 1200000.00m) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1, results[0].Count); + } + + [ConditionalFact] + public virtual void Count_Col_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Count(e.Salary, () => e.Salary != 500000.00m) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1, results[0].Count); + } + + #endregion + + #region Average Tests + + [ConditionalFact] + public virtual void Avg_Decimal() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + AverageSalary = EF.Functions.Over().Average(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(496875.111250m, results[0].AverageSalary); + } + + [ConditionalFact] + public virtual void Avg_Int() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + AverageWork = EF.Functions.Over().Average(e.WorkExperience) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(11, results[0].AverageWork); + } + + [ConditionalFact] + public virtual void Avg_Decimal_Int_Cast_Decimal() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + AverageWork = EF.Functions.Over().Average(e.WorkExperience) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(11.375m, results[0].AverageWork); + } + + [ConditionalFact] + public virtual void Avg_Null() + { + using var context = CreateContext(); + + var results = context.NullTestEmployees.Select(e => new + { + e.Id, + e.Name, + AverageSalary = EF.Functions.Over().Average(e.Salary) + }).ToList(); + + Assert.Equal(2, results.Count); + Assert.Null(results[0].AverageSalary); + } + + [ConditionalFact] + public virtual void Avg_Filter() + { + using var context = CreateContext(); + + var ids = new int[] { 1, 2, 3 }; + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Avg = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Average(e.Salary, () => ids.Contains(e.EmployeeId)) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].Avg); + } + + #endregion + + #region Sum Tests + + [ConditionalFact] + public virtual void Sum_Decimal() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + SumSalary = EF.Functions.Over().Sum(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(3975000.89m, results[0].SumSalary); + } + + [ConditionalFact] + public virtual void Sum_Int() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + SumWorkExperience = EF.Functions.Over().Sum(e.WorkExperience) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(91, results[0].SumWorkExperience); + } + + [ConditionalFact] + public virtual void Sum_Null() + { + using var context = CreateContext(); + + var results = context.NullTestEmployees.Select(e => new + { + e.Id, + e.Name, + Sum = EF.Functions.Over().Sum(e.Salary) + }).ToList(); + + Assert.Equal(2, results.Count); + Assert.Null(results[0].Sum); + } + + [ConditionalFact] + public virtual void Sum_Filter() + { + using var context = CreateContext(); + + var ids = new int[] { 1, 2, 3 }; + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Sum = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Sum(e.Salary, () => ids.Contains(e.EmployeeId)) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].Sum); + } + + #endregion + + [ConditionalFact] + public virtual void RowNumber_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + RowNumber = EF.Functions.Over().OrderBy(e.Name).RowNumber() + }).ToList(); + + Assert.Equal(8, results.Count); + + for (int i = 0; i < results.Count; i++) + { + Assert.Equal(i + 1, results[i].RowNumber); + } + } + + [ConditionalFact] + public virtual void First_Value_OderByEnd_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + FirstValue = EF.Functions.Over().OrderBy(e.Salary).FirstValue(e.Name) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal("Commander Cody", results[0].FirstValue); + } + + [ConditionalFact] + public virtual void First_Value_FrameEnd_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + FirstValue = EF.Functions.Over().OrderBy(e.Salary).Rows(RowsPreceding.CurrentRow, RowsFollowing.UnboundedFollowing).FirstValue(e.Name) + }).ToList(); + + Assert.Equal(8, results.Count); + + foreach(var row in results) + Assert.Equal(results[0].Name, results[0].FirstValue); + } + + [ConditionalFact] + public virtual void First_Value_Null() + { + using var context = CreateContext(); + + var results = context.NullTestEmployees.Select(e => new + { + e.Id, + e.Name, + FirstValue = EF.Functions.Over().OrderBy(e.WorkExperience).FirstValue(e.Salary) + }).ToList(); + + Assert.Equal(2, results.Count); + Assert.Null(results[0].FirstValue); + } + + [ConditionalFact] + public virtual void Last_Value_OderByEnd_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + LastValue = EF.Functions.Over().OrderBy(e.Salary).LastValue(e.Name) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal("Commander Cody", results[0].LastValue); + } + + [ConditionalFact] + public virtual void Last_Value_FrameEnd_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + LastValue = EF.Functions.Over().OrderBy(e.Salary).Rows(RowsPreceding.CurrentRow, RowsFollowing.UnboundedFollowing).LastValue(e.Name) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal("Jabba the Hutt", results[0].LastValue); + } + + [ConditionalFact] + public virtual void Last_Value_Null() + { + using var context = CreateContext(); + + var results = context.NullTestEmployees.Select(e => new + { + e.Id, + e.Name, + LastValue = EF.Functions.Over().OrderBy(e.WorkExperience).LastValue(e.Salary) + }).ToList(); + + Assert.Equal(2, results.Count); + Assert.Null(results[0].LastValue); + } + + [ConditionalFact] + public virtual void Rank_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Rank = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.WorkExperience).Rank() + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Equal(3, results[0].Id); + Assert.Equal(1, results[0].Rank); + + Assert.Equal(1, results[1].Id); + Assert.Equal(1, results[1].Rank); + + Assert.Equal(4, results[2].Id); + Assert.Equal(2, results[2].Rank); + + Assert.Equal(6, results[3].Id); + Assert.Equal(1, results[3].Rank); + + Assert.Equal(7, results[4].Id); + Assert.Equal(2, results[4].Rank); + + Assert.True(results[5].Id == 5 || results[5].Id == 8); + Assert.Equal(1, results[5].Rank); + + Assert.True(results[6].Id == 5 || results[6].Id == 8); + Assert.Equal(1, results[6].Rank); + + Assert.Equal(2, results[7].Id); + Assert.Equal(3, results[7].Rank); + } + + [ConditionalFact] + public virtual void Dense_Rank_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Rank = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.WorkExperience).DenseRank() + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Equal(3, results[0].Id); + Assert.Equal(1, results[0].Rank); + + Assert.Equal(1, results[1].Id); + Assert.Equal(1, results[1].Rank); + + Assert.Equal(4, results[2].Id); + Assert.Equal(2, results[2].Rank); + + Assert.Equal(6, results[3].Id); + Assert.Equal(1, results[3].Rank); + + Assert.Equal(7, results[4].Id); + Assert.Equal(2, results[4].Rank); + + Assert.True(results[5].Id == 5 || results[5].Id == 8); + Assert.Equal(1, results[5].Rank); + + Assert.True(results[6].Id == 5 || results[6].Id == 8); + Assert.Equal(1, results[6].Rank); + + Assert.Equal(2, results[7].Id); + Assert.Equal(2, results[7].Rank); + } + + [ConditionalFact] + public virtual void NTile_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Rank = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.WorkExperience).NTile(3) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Equal(3, results[0].Id); + Assert.Equal(1, results[0].Rank); + + Assert.Equal(1, results[1].Id); + Assert.Equal(1, results[1].Rank); + + Assert.Equal(4, results[2].Id); + Assert.Equal(2, results[2].Rank); + + Assert.Equal(6, results[3].Id); + Assert.Equal(1, results[3].Rank); + + Assert.Equal(7, results[4].Id); + Assert.Equal(2, results[4].Rank); + + Assert.True(results[5].Id == 5 || results[5].Id == 8); + Assert.Equal(1, results[5].Rank); + + Assert.True(results[6].Id == 5 || results[6].Id == 8); + Assert.Equal(2, results[6].Rank); + + Assert.Equal(2, results[7].Id); + Assert.Equal(3, results[7].Rank); + } + + [ConditionalFact] + public virtual void Percent_Rank_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + PercentRank = EF.Functions.Over().OrderBy(e.Salary).PercentRank() + }).ToList(); + + //todo - this test might need to be altered for sqlite due to precision + + Assert.Equal(8, results.Count); + Assert.Equal(0, results[0].PercentRank); + Assert.Equal(0.1428571, Math.Round(results[1].PercentRank, 7)); + Assert.Equal(0.2857143, Math.Round(results[2].PercentRank, 7)); + Assert.Equal(0.4285714, Math.Round(results[3].PercentRank, 7)); + Assert.Equal(0.5714286, Math.Round(results[4].PercentRank, 7)); + Assert.Equal(0.7142857, Math.Round(results[5].PercentRank, 7)); + Assert.Equal(0.8571429, Math.Round(results[6].PercentRank, 7)); + Assert.Equal(1, results[7].PercentRank); + } + + + [ConditionalFact] + public virtual void Cume_Dist_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + CumeDist = EF.Functions.Over().OrderBy(e.Salary).CumeDist() + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(0.125, results[0].CumeDist); + Assert.Equal(0.25, results[1].CumeDist); + Assert.Equal(0.375, results[2].CumeDist); + Assert.Equal(0.5, results[3].CumeDist); + Assert.Equal(0.625, results[4].CumeDist); + Assert.Equal(0.75, results[5].CumeDist); + Assert.Equal(0.875, results[6].CumeDist); + Assert.Equal(1, results[7].CumeDist); + } + + [ConditionalFact] + public virtual void Lag_Decimal_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + PreviousSalary = EF.Functions.Over().OrderBy(e.Salary).Lag(e.Salary, 1, 0.0m) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(0, results[0].PreviousSalary); + Assert.Equal(25000.12m, results[1].PreviousSalary); + Assert.Equal(50000.00m, results[2].PreviousSalary); + Assert.Equal(100000.00m, results[3].PreviousSalary); + Assert.Equal(200000.00m, results[4].PreviousSalary); + Assert.Equal(350000.24m, results[5].PreviousSalary); + Assert.Equal(500000.00m, results[6].PreviousSalary); + Assert.Equal(1000000.53m, results[7].PreviousSalary); + } + + [ConditionalFact] + public virtual void Lag_Int_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + PreviousId = EF.Functions.Over().OrderBy(e.Id).Lag(e.Id, 1, 0) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(0, results[0].PreviousId); + Assert.Equal(1, results[1].PreviousId); + Assert.Equal(2, results[2].PreviousId); + Assert.Equal(3, results[3].PreviousId); + Assert.Equal(4, results[4].PreviousId); + Assert.Equal(5, results[5].PreviousId); + Assert.Equal(6, results[6].PreviousId); + Assert.Equal(7, results[7].PreviousId); + } + + [ConditionalFact] + public virtual void Lag_String_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + PreviousName = EF.Functions.Over().OrderBy(e.Name).Lag(e.Name, 1, "test") + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal("test", results[0].PreviousName); + Assert.Equal(results[0].Name, results[1].PreviousName); + Assert.Equal(results[1].Name, results[2].PreviousName); + Assert.Equal(results[2].Name, results[3].PreviousName); + Assert.Equal(results[3].Name, results[4].PreviousName); + Assert.Equal(results[4].Name, results[5].PreviousName); + Assert.Equal(results[5].Name, results[6].PreviousName); + Assert.Equal(results[6].Name, results[7].PreviousName); + } + + [ConditionalFact] + public virtual void Lead_Decimal_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + NextSalary = EF.Functions.Over().OrderBy(e.Salary).Lead(e.Salary, 1, 0.0m) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Equal(50000.00m, results[0].NextSalary); + Assert.Equal(100000.00m, results[1].NextSalary); + Assert.Equal(200000.00m, results[2].NextSalary); + Assert.Equal(350000.24m, results[3].NextSalary); + Assert.Equal(500000.00m, results[4].NextSalary); + Assert.Equal(1000000.53m, results[5].NextSalary); + Assert.Equal(1750000.00m, results[6].NextSalary); + Assert.Equal(0, results[7].NextSalary); + } + + [ConditionalFact] + public virtual void Lead_Int_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + NextId = EF.Functions.Over().OrderBy(e.Id).Lead(e.Id, 1, 0) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Equal(2, results[0].NextId); + Assert.Equal(3, results[1].NextId); + Assert.Equal(4, results[2].NextId); + Assert.Equal(5, results[3].NextId); + Assert.Equal(6, results[4].NextId); + Assert.Equal(7, results[5].NextId); + Assert.Equal(8, results[6].NextId); + Assert.Equal(0, results[7].NextId); + } + + #endregion + + #region WindowOverExpression Equality tests + + [ConditionalFact] + public virtual void Multiple_Aggregates_Basic_NoDup_Query() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().Max(e.Salary), + MinSalary = EF.Functions.Over().Min(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1750000.0m, results[0].MaxSalary); + Assert.Equal(25000.12m, results[0].MinSalary); + } + + [ConditionalFact] + public virtual void Multiple_Aggregates_Basic_Dup_Query() + { + + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary1 = EF.Functions.Over().Max(e.Salary), + MaxSalary2 = EF.Functions.Over().Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1750000.0m, results[0].MaxSalary1); + Assert.Equal(1750000.0m, results[0].MaxSalary2); + } + + #endregion + + #region Rows / Range Tests + + #region Rows(int preceding) + + [ConditionalFact] + public virtual void Rows_Preceding_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #region Rows(RowsPreceding preceding) + + [ConditionalFact] + public virtual void Rows_Preceding_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Rows_Preceding_UnboundedPreceding() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.UnboundedPreceding).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #region Rows(int preceding, int following) + + [ConditionalFact] + public virtual void Rows_Preceding_X_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(1, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #region Rows(RowsPreceding preceding, int following) + + [ConditionalFact] + public virtual void Rows_Preceding_CurrentRow_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.CurrentRow, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Rows_Preceding_UnboundedPreceding_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.UnboundedPreceding, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #region Rows(int preceding, RowsFollowing following) + + [ConditionalFact] + public virtual void Rows_Preceding_X_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Rows_Preceding_X_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #region Rows(RowsPreceding preceding, RowsFollowing following) + + [ConditionalFact] + public virtual void Rows_Preceding_CurrentRow_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.CurrentRow, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Rows_Preceding_CurrentRow_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.CurrentRow, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Rows_Preceding_UnboundedPreceding_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.UnboundedPreceding, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Rows_Preceding_UnboundedPreceding_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(RowsPreceding.UnboundedPreceding, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #region Range(RowsPreceding preceding) + + [ConditionalFact] + public virtual void Range_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Range_UnboundedPreceding() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.UnboundedPreceding).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #region Range(RowsPreceding preceding, RowsFollowing following) + + [ConditionalFact] + public virtual void Range_Preceding_CurrentRow_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.CurrentRow, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Range_Preceding_CurrentRow_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.CurrentRow, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Range_Preceding_UnboundedPreceding_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.UnboundedPreceding, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Range_Preceding_UnboundedPreceding_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.UnboundedPreceding, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + #endregion + + #endregion + + #region Partition / Order By + + [ConditionalFact] + public virtual void Rows_No_Parition() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().OrderBy(e.Name).Rows(1, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(500000.00m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Range_No_Parition() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().OrderBy(e.Name).Range(RowsPreceding.CurrentRow, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).OrderBy(r => r.Name).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1750000.00m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void OrderBy_No_Parition() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().OrderBy(e.Name).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(50000.00m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void OrderBy_Desc_No_Parition() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().OrderByDescending(e.Name).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(100000.00m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void OrderBy_Desc_Rows() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().OrderByDescending(e.Name).Rows(1).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(100000.00m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Outer_Order_By_Sql() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Rank = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.WorkExperience).ThenBy(e.Name).Rank() + }).OrderBy(r => r.Name).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(5, results[0].Id); + } + + [ConditionalFact] + public virtual void Partition_No_OrderBy_No_Frame() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Partition_MultipleColumns() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName, e.WorkExperience).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + } + + [ConditionalFact] + public virtual void Partition_ColumnModified() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.WorkExperience / 10).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(350000.24m, results[0].MaxSalary); + } + + #endregion + + #region Nullability + + [ConditionalFact] + public virtual void NullTestBang() + { + using var context = CreateContext(); + + var ids = new int[] { 1, 2, 3 }; + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + SumIn = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Sum(e.Salary, () => ids.Contains(e.EmployeeId)), + SumNotIn = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Sum(e.Salary, () => !ids.Contains(e.EmployeeId)) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Null(results[0].SumNotIn); + Assert.Equal(1000000.53m, results[0].SumIn); + } + + [ConditionalFact] + public virtual void NullTestEquals() + { + using var context = CreateContext(); + + var ids = new int[] { 1, 2, 3 }; + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + SumIn = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Sum(e.Salary, () => ids.Contains(e.EmployeeId) == true), + SumNotIn = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Sum(e.Salary, () => ids.Contains(e.EmployeeId) == false) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Null(results[0].SumNotIn); + Assert.Equal(1000000.53m, results[0].SumIn); + } + + #endregion + + //todo - have the results of one window function be used by a second. + + #region Where + + /*[ConditionalFact] + public virtual void WindowFunctionInWhere() + { + using var context = CreateContext(); + + var results = context.Employees + .Where(e => EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).RowNumber() == 1) + .Select(e => new + { + e.Id, + e.Name, + }).ToList(); + + Assert.Equal(1, results.Count); + }*/ + + #endregion + + #region OrderBy + + [ConditionalFact] + public virtual void WindowFunctionInOrderBy() + { + using var context = CreateContext(); + + var results = context.Employees + .OrderBy(e => EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).RowNumber()) + .Select(e => new + { + e.Id, + e.Name, + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(3, results[0].Id); + Assert.Equal(4, results[1].Id); + Assert.Equal(6, results[2].Id); + } + + #endregion + + #endregion +} + diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs new file mode 100644 index 00000000000..31a1a0225ab --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs @@ -0,0 +1,1247 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class WindowFunctionSqlServerTest : WindowFunctionTestBase +{ + public WindowFunctionSqlServerTest(SqlServer fixture, ITestOutputHelper testOutputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public class SqlServer : WindowFunctionFixture + { + protected override string StoreName => "WindowFunctionTests"; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + } + + #region Tests + + #region Window Functions Tests + + #region Max Tests + + public override void Max_Basic() + { + base.Max_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER () AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Max_Parition_Order_Rows() + { + base.Max_Parition_Order_Rows(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Max_Null() + { + base.Max_Null(); + + AssertSql( + """ +SELECT [n].[Id], [n].[Name], MAX([n].[Salary]) OVER () AS [MaxSalary] +FROM [NullTestEmployees] AS [n] +"""); + } + + public override void Max_Filter() + { + base.Max_Filter(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MAX(CASE + WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] + ELSE NULL +END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Min Tests + + public override void Min_Basic() + { + base.Min_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MIN([e].[Salary]) OVER () AS [MinSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Min_Null() + { + base.Min_Null(); + + AssertSql( + """ +SELECT [n].[Id], [n].[Name], MIN([n].[Salary]) OVER () AS [MinSalary] +FROM [NullTestEmployees] AS [n] +"""); + } + + public override void Min_Filter() + { + base.Min_Filter(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MIN(CASE + WHEN [e].[Salary] = 200000.0 THEN [e].[Salary] + ELSE NULL +END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [MinSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Count Tests + + public override void Count_Star_Basic() + { + base.Count_Star_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT(*) OVER () AS [Count] +FROM [Employees] AS [e] +"""); + } + + public override void Count_Col_Basic() + { + base.Count_Col_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT([e].[Id]) OVER () AS [Count] +FROM [Employees] AS [e] +"""); + } + + public override void Count_Star_Filter() + { + base.Count_Star_Filter(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT(CASE + WHEN [e].[Salary] <= 1200000.0 THEN N'1' + ELSE NULL +END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Count] +FROM [Employees] AS [e] +"""); + } + + public override void Count_Col_Filter() + { + base.Count_Col_Filter(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT(CASE + WHEN [e].[Salary] <> 500000.0 THEN [e].[Salary] + ELSE NULL +END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Count] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Average Tests + + public override void Avg_Decimal() + { + base.Avg_Decimal(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], AVG([e].[Salary]) OVER () AS [AverageSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Avg_Int() + { + base.Avg_Int(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], AVG([e].[WorkExperience]) OVER () AS [AverageWork] +FROM [Employees] AS [e] +"""); + } + + public override void Avg_Decimal_Int_Cast_Decimal() + { + base.Avg_Decimal_Int_Cast_Decimal(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], AVG(CAST([e].[WorkExperience] AS decimal(18,2))) OVER () AS [AverageWork] +FROM [Employees] AS [e] +"""); + } + + public override void Avg_Null() + { + base.Avg_Null(); + + AssertSql( + """ +SELECT [n].[Id], [n].[Name], AVG([n].[Salary]) OVER () AS [AverageSalary] +FROM [NullTestEmployees] AS [n] +"""); + } + + public override void Avg_Filter() + { + base.Avg_Filter(); + + AssertSql( + """ +@__ids_1='[1,2,3]' (Size = 4000) + +SELECT [e].[Id], [e].[Name], AVG(CASE + WHEN [e].[EmployeeId] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] + ) THEN [e].[Salary] + ELSE NULL +END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Avg] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Sum Tests + + public override void Sum_Decimal() + { + base.Sum_Decimal(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], SUM([e].[Salary]) OVER () AS [SumSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Sum_Int() + { + base.Sum_Int(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], SUM([e].[WorkExperience]) OVER () AS [SumWorkExperience] +FROM [Employees] AS [e] +"""); + } + + public override void Sum_Null() + { + base.Sum_Null(); + + AssertSql( + """ +SELECT [n].[Id], [n].[Name], SUM([n].[Salary]) OVER () AS [Sum] +FROM [NullTestEmployees] AS [n] +"""); + } + + public override void Sum_Filter() + { + base.Sum_Filter(); + + AssertSql( + """ +@__ids_1='[1,2,3]' (Size = 4000) + +SELECT [e].[Id], [e].[Name], SUM(CASE + WHEN [e].[EmployeeId] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] + ) THEN [e].[Salary] + ELSE NULL +END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Sum] +FROM [Employees] AS [e] +"""); + } + + #endregion + + public override void RowNumber_Basic() + { + base.RowNumber_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], ROW_NUMBER() OVER ( ORDER BY [e].[Name]) AS [RowNumber] +FROM [Employees] AS [e] +"""); + } + + public override void First_Value_OderByEnd_Basic() + { + base.First_Value_OderByEnd_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], FIRST_VALUE([e].[Name]) OVER ( ORDER BY [e].[Salary]) AS [FirstValue] +FROM [Employees] AS [e] +"""); + } + + public override void First_Value_FrameEnd_Basic() + { + base.First_Value_FrameEnd_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], FIRST_VALUE([e].[Name]) OVER ( ORDER BY [e].[Salary] ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS [FirstValue] +FROM [Employees] AS [e] +"""); + } + + public override void First_Value_Null() + { + base.First_Value_Null(); + + AssertSql( + """ +SELECT [n].[Id], [n].[Name], FIRST_VALUE([n].[Salary]) OVER ( ORDER BY [n].[WorkExperience]) AS [FirstValue] +FROM [NullTestEmployees] AS [n] +"""); + } + + public override void Last_Value_OderByEnd_Basic() + { + base.Last_Value_OderByEnd_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], LAST_VALUE([e].[Name]) OVER ( ORDER BY [e].[Salary]) AS [LastValue] +FROM [Employees] AS [e] +"""); + } + + public override void Last_Value_FrameEnd_Basic() + { + base.Last_Value_FrameEnd_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], LAST_VALUE([e].[Name]) OVER ( ORDER BY [e].[Salary] ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS [LastValue] +FROM [Employees] AS [e] +"""); + } + + public override void Last_Value_Null() + { + base.Last_Value_Null(); + + AssertSql( + """ +SELECT [n].[Id], [n].[Name], LAST_VALUE([n].[Salary]) OVER ( ORDER BY [n].[WorkExperience]) AS [LastValue] +FROM [NullTestEmployees] AS [n] +"""); + } + + public override void Rank_Basic() + { + base.Rank_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], RANK() OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[WorkExperience]) AS [Rank] +FROM [Employees] AS [e] +"""); + } + + public override void Dense_Rank_Basic() + { + base.Dense_Rank_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], DENSE_RANK() OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[WorkExperience]) AS [Rank] +FROM [Employees] AS [e] +"""); + } + + public override void NTile_Basic() + { + base.NTile_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], NTILE(3) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[WorkExperience]) AS [Rank] +FROM [Employees] AS [e] +"""); + } + + public override void Percent_Rank_Basic() + { + base.Percent_Rank_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], PERCENT_RANK() OVER ( ORDER BY [e].[Salary]) AS [PercentRank] +FROM [Employees] AS [e] +"""); + } + + public override void Cume_Dist_Basic() + { + base.Cume_Dist_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], CUME_DIST() OVER ( ORDER BY [e].[Salary]) AS [CumeDist] +FROM [Employees] AS [e] +"""); + } + + public override void Lag_Decimal_Basic() + { + base.Lag_Decimal_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], LAG([e].[Salary], 1, 0.0) OVER ( ORDER BY [e].[Salary]) AS [PreviousSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Lag_Int_Basic() + { + base.Lag_Int_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], LAG([e].[Id], 1, 0) OVER ( ORDER BY [e].[Id]) AS [PreviousId] +FROM [Employees] AS [e] +"""); + } + + public override void Lag_String_Basic() + { + base.Lag_String_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], LAG([e].[Name], 1, N'test') OVER ( ORDER BY [e].[Name]) AS [PreviousName] +FROM [Employees] AS [e] +"""); + } + + public override void Lead_Decimal_Basic() + { + base.Lead_Decimal_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], LEAD([e].[Salary], 1, 0.0) OVER ( ORDER BY [e].[Salary]) AS [NextSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Lead_Int_Basic() + { + base.Lead_Int_Basic(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], LEAD([e].[Id], 1, 0) OVER ( ORDER BY [e].[Id]) AS [NextId] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region WindowOverExpression Equality tests + + public override void Multiple_Aggregates_Basic_NoDup_Query() + { + base.Multiple_Aggregates_Basic_NoDup_Query(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER () AS [MaxSalary], MIN([e].[Salary]) OVER () AS [MinSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Multiple_Aggregates_Basic_Dup_Query() + { + base.Multiple_Aggregates_Basic_Dup_Query(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER () AS [MaxSalary1] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region SQL Server Specific + + [ConditionalFact] + public void Count_Big_Star_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().CountBig() + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(8, results[0].Count); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT_BIG(*) OVER () AS [Count] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void Count_Big_Col_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().CountBig(e.Id) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(8, results[0].Count); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT_BIG([e].[Id]) OVER () AS [Count] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void Count_Big_Star_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().CountBig(() => e.Salary > 10m) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(8, results[0].Count); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT_BIG(CASE + WHEN [e].[Salary] > 10.0 THEN N'1' + ELSE NULL +END) OVER () AS [Count] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void Count_Big_Col_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Count = EF.Functions.Over().CountBig(e.Id, () => e.Salary > 10m) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(8, results[0].Count); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], COUNT_BIG(CASE + WHEN [e].[Salary] > 10.0 THEN [e].[Id] + ELSE NULL +END) OVER () AS [Count] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void Stdev_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + StdDev = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).Stdev(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Null(results[0].StdDev); + Assert.Equal(17677.58468d, Math.Round(results[1].StdDev!.Value, 5)); + Assert.Equal(180854.55298d, Math.Round(results[2].StdDev!.Value, 5)); + Assert.Equal(149129.50692d, Math.Round(results[3].StdDev!.Value, 5)); + Assert.Equal(132759.24601d, Math.Round(results[4].StdDev!.Value, 5)); + Assert.Equal(187361.07404d, Math.Round(results[5].StdDev!.Value, 5)); + Assert.Equal(608789.76503d, Math.Round(results[6].StdDev!.Value, 5)); + Assert.Equal(599171.71693d, Math.Round(results[7].StdDev!.Value, 5)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], STDEV([e].[Salary]) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void Stdev_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + StdDev = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).Stdev(e.Salary, () => e.Salary > 100000m) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Null(results[0].StdDev); + Assert.Null(results[1].StdDev); + Assert.Null(results[2].StdDev); + Assert.Null(results[3].StdDev); + Assert.Equal(106066.18688d, Math.Round(results[4].StdDev!.Value, 5)); + Assert.Equal(150000.00000d, Math.Round(results[5].StdDev!.Value, 5)); + Assert.Equal(710633.48078d, Math.Round(results[6].StdDev!.Value, 5)); + Assert.Equal(629880.95256d, Math.Round(results[7].StdDev!.Value, 5)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], STDEV(CASE + WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] + ELSE NULL +END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void StdevP_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + StdDevP = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).StdevP(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Equal(0f, Math.Round(results[0].StdDevP ?? 0, 5)); + Assert.Equal(12499.94d, Math.Round(results[1].StdDevP ?? 0, 5)); + Assert.Equal(147667.12415d, Math.Round(results[2].StdDevP ?? 0, 5)); + Assert.Equal(129149.94144d, Math.Round(results[3].StdDevP ?? 0, 5)); + Assert.Equal(118743.47948d, Math.Round(results[4].StdDevP ?? 0, 5)); + Assert.Equal(171036.47775d, Math.Round(results[5].StdDevP ?? 0, 5)); + Assert.Equal(563629.80100d, Math.Round(results[6].StdDevP ?? 0, 5)); + Assert.Equal(560473.82015d, Math.Round(results[7].StdDevP ?? 0, 5)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], STDEVP([e].[Salary]) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDevP] +FROM [Employees] AS [e] +"""); + } + + + [ConditionalFact] + public void StdevP_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + StdDev = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).StdevP(e.Salary, () => e.Salary > 100000m) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Null(results[0].StdDev); + Assert.Null(results[1].StdDev); + Assert.Equal(0, results[2].StdDev); + Assert.Equal(0, results[3].StdDev); + Assert.Equal(75000.12d, Math.Round(results[4].StdDev!.Value, 5)); + Assert.Equal(122474.48714d, Math.Round(results[5].StdDev!.Value, 5)); + Assert.Equal(615426.64713d, Math.Round(results[6].StdDev!.Value, 5)); + Assert.Equal(563382.65106d, Math.Round(results[7].StdDev!.Value, 5)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], STDEVP(CASE + WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] + ELSE NULL +END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void Var_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + Var = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).Var(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Null(results[0].Var); + Assert.Equal(312497000.007d, Math.Round(results[1].Var!.Value, 3)); + Assert.Equal(32708369333.348d, Math.Round(results[2].Var!.Value, 3)); + Assert.Equal(22239609833.347d, Math.Round(results[3].Var!.Value, 3)); + Assert.Equal(17625017400.012d, Math.Round(results[4].Var!.Value, 3)); + Assert.Equal(35104172066.677d, Math.Round(results[5].Var!.Value, 3)); + Assert.Equal(370624978000.009d, Math.Round(results[6].Var!.Value, 3)); + Assert.Equal(359006746366.108d, Math.Round(results[7].Var!.Value, 3)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], VAR([e].[Salary]) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [Var] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void Var_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + StdDev = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).Var(e.Salary, () => e.Salary > 100000m) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Null(results[0].StdDev); + Assert.Null(results[1].StdDev); + Assert.Null(results[2].StdDev); + Assert.Null(results[3].StdDev); + Assert.Equal(11250036000.02878d, Math.Round(results[4].StdDev!.Value, 5)); + Assert.Equal(22500000000.0192d, Math.Round(results[5].StdDev!.Value, 5)); + Assert.Equal(504999944000.01434d, Math.Round(results[6].StdDev!.Value, 5)); + Assert.Equal(396750014400.05493d, Math.Round(results[7].StdDev!.Value, 5)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], VAR(CASE + WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] + ELSE NULL +END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void VarP_Basic() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + VarP = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).VarP(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Equal(0d, Math.Round(results[0].VarP ?? 0, 3)); + Assert.Equal(156248500.004d, Math.Round(results[1].VarP ?? 0, 3)); + Assert.Equal(21805579555.565d, Math.Round(results[2].VarP ?? 0, 3)); + Assert.Equal(16679707375.01d, Math.Round(results[3].VarP ?? 0, 3)); + Assert.Equal(14100013920.009d, Math.Round(results[4].VarP ?? 0, 3)); + Assert.Equal(29253476722.231d, Math.Round(results[5].VarP ?? 0, 3)); + Assert.Equal(317678552571.436d, Math.Round(results[6].VarP ?? 0, 3)); + Assert.Equal(314130903070.344d, Math.Round(results[7].VarP ?? 0, 3)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], VARP([e].[Salary]) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [VarP] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public void VarP_Filter() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + StdDev = EF.Functions.Over().OrderBy(e.WorkExperience).ThenBy(e.Name).VarP(e.Salary, () => e.Salary > 100000m) + }).ToList(); + + Assert.Equal(8, results.Count); + + Assert.Null(results[0].StdDev); + Assert.Null(results[1].StdDev); + Assert.Equal(0, results[2].StdDev); + Assert.Equal(0, results[3].StdDev); + Assert.Equal(5625018000.01439d, Math.Round(results[4].StdDev!.Value, 5)); + Assert.Equal(15000000000.0128d, Math.Round(results[5].StdDev!.Value, 5)); + Assert.Equal(378749958000.01074d, Math.Round(results[6].StdDev!.Value, 5)); + Assert.Equal(317400011520.04395d, Math.Round(results[7].StdDev!.Value, 5)); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], VARP(CASE + WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] + ELSE NULL +END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Rows / Range Tests + + #region Rows(int preceding) + + public override void Rows_Preceding_X() + { + base.Rows_Preceding_X(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS 2 PRECEDING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Rows(RowsPreceding preceding) + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow() + { + base.Rows_Preceding_CurrentRow(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS CURRENT ROW) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding() + { + base.Rows_Preceding_UnboundedPreceding(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS UNBOUNDED PRECEDING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Rows(int preceding, int following) + + [ConditionalFact] + public override void Rows_Preceding_X_Following_X() + { + base.Rows_Preceding_X_Following_X(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Rows(RowsPreceding preceding, int following) + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow_Following_X() + { + base.Rows_Preceding_CurrentRow_Following_X(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding_Following_X() + { + base.Rows_Preceding_UnboundedPreceding_Following_X(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN UNBOUNDED PRECEDING AND 2 FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Rows(int preceding, RowsFollowing following) + + [ConditionalFact] + public override void Rows_Preceding_X_Following_CurrentRow() + { + base.Rows_Preceding_X_Following_CurrentRow(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_X_Following_UnboundedFollowing() + { + base.Rows_Preceding_X_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN 2 PRECEDING AND UNBOUNDED FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Rows(RowsPreceding preceding, RowsFollowing following) + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow_Following_CurrentRow() + { + base.Rows_Preceding_CurrentRow_Following_CurrentRow(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN CURRENT ROW AND CURRENT ROW) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow_Following_UnboundedFollowing() + { + base.Rows_Preceding_CurrentRow_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding_Following_CurrentRow() + { + base.Rows_Preceding_UnboundedPreceding_Following_CurrentRow(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding_Following_UnboundedFollowing() + { + base.Rows_Preceding_UnboundedPreceding_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion Range(RowsPreceding preceding) + + #region Range(RowsPreceding preceding) + + [ConditionalFact] + public override void Range_CurrentRow() + { + base.Range_CurrentRow(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] RANGE CURRENT ROW) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Range_UnboundedPreceding() + { + base.Range_UnboundedPreceding(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] RANGE UNBOUNDED PRECEDING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Range(RowsPreceding preceding, RowsFollowing following) + + #endregion + + [ConditionalFact] + public override void Range_Preceding_CurrentRow_Following_CurrentRow() + { + base.Range_Preceding_CurrentRow_Following_CurrentRow(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] RANGE BETWEEN CURRENT ROW AND CURRENT ROW) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Range_Preceding_CurrentRow_Following_UnboundedFollowing() + { + base.Range_Preceding_CurrentRow_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Range_Preceding_UnboundedPreceding_Following_CurrentRow() + { + base.Range_Preceding_UnboundedPreceding_Following_CurrentRow(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + [ConditionalFact] + public override void Range_Preceding_UnboundedPreceding_Following_UnboundedFollowing() + { + base.Range_Preceding_UnboundedPreceding_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name] RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + #endregion + + #region Partition / Order By + + public override void Rows_No_Parition() + { + base.Rows_No_Parition(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER ( ORDER BY [e].[Name] ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Range_No_Parition() + { + base.Range_No_Parition(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER ( ORDER BY [e].[Name] RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS [MaxSalary] +FROM [Employees] AS [e] +ORDER BY [e].[Name] +"""); + } + + public override void OrderBy_No_Parition() + { + base.OrderBy_No_Parition(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER ( ORDER BY [e].[Name]) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void OrderBy_Desc_No_Parition() + { + base.OrderBy_Desc_No_Parition(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER ( ORDER BY [e].[Name] DESC) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Partition_No_OrderBy_No_Frame() + { + base.Partition_No_OrderBy_No_Frame(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName]) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Partition_MultipleColumns() + { + base.Partition_MultipleColumns(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[DepartmentName], [e].[WorkExperience]) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Partition_ColumnModified() + { + base.Partition_ColumnModified(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER (PARTITION BY [e].[WorkExperience] / 10) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void OrderBy_Desc_Rows() + { + base.OrderBy_Desc_Rows(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], MAX([e].[Salary]) OVER ( ORDER BY [e].[Name] DESC ROWS 1 PRECEDING) AS [MaxSalary] +FROM [Employees] AS [e] +"""); + } + + public override void Outer_Order_By_Sql() + { + base.Outer_Order_By_Sql(); + + AssertSql( +""" +SELECT [e].[Id], [e].[Name], RANK() OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[WorkExperience], [e].[Name]) AS [Rank] +FROM [Employees] AS [e] +ORDER BY [e].[Name] +"""); + } + + #endregion + + #endregion + + #region Where + + #endregion + + #region Order By + + public override void WindowFunctionInOrderBy() + { + base.WindowFunctionInOrderBy(); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name] +FROM [Employees] AS [e] +ORDER BY ROW_NUMBER() OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) +"""); + } + + #endregion + + public void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs new file mode 100644 index 00000000000..f08280bd959 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs @@ -0,0 +1,1407 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Sqlite.Query; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class WindowFunctionSqliteTest : WindowFunctionTestBase +{ + public WindowFunctionSqliteTest(Sqlite fixture, ITestOutputHelper testOutputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public class Sqlite : WindowFunctionFixture + { + protected override string StoreName => "WindowFunctionTests"; + + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + } + + #region Tests + + #region Base Window Functions Tests + + #region Max Tests + + public override void Max_Basic() + { + base.Max_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER () AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Max_Parition_Order_Rows() + { + base.Max_Parition_Order_Rows(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Max_Null() + { + base.Max_Null(); + + AssertSql( + """ +SELECT "n"."Id", "n"."Name", MAX("n"."Salary") OVER () AS "MaxSalary" +FROM "NullTestEmployees" AS "n" +"""); + } + + public override void Max_Filter() + { + base.Max_Filter(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX(CASE + WHEN ef_compare("e"."Salary", '100000.0') > 0 THEN "e"."Salary" + ELSE NULL +END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Min Tests + + public override void Min_Basic() + { + base.Min_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MIN("e"."Salary") OVER () AS "MinSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Min_Null() + { + base.Min_Null(); + + AssertSql( + """ +SELECT "n"."Id", "n"."Name", MIN("n"."Salary") OVER () AS "MinSalary" +FROM "NullTestEmployees" AS "n" +"""); + } + + public override void Min_Filter() + { + base.Min_Filter(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MIN(CASE + WHEN "e"."Salary" = '200000.0' THEN "e"."Salary" + ELSE NULL +END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "MinSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Count Tests + + public override void Count_Star_Basic() + { + base.Count_Star_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", COUNT(*) OVER () AS "Count" +FROM "Employees" AS "e" +"""); + } + + public override void Count_Col_Basic() + { + base.Count_Col_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", COUNT("e"."Id") OVER () AS "Count" +FROM "Employees" AS "e" +"""); + } + + public override void Count_Star_Filter() + { + base.Count_Star_Filter(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", COUNT(CASE + WHEN ef_compare("e"."Salary", '1200000.0') <= 0 THEN '1' + ELSE NULL +END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Count" +FROM "Employees" AS "e" +"""); + } + + public override void Count_Col_Filter() + { + base.Count_Col_Filter(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", COUNT(CASE + WHEN "e"."Salary" <> '500000.0' THEN "e"."Salary" + ELSE NULL +END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Count" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Average Tests + + public override void Avg_Decimal() + { + base.Avg_Decimal(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", AVG("e"."Salary") OVER () AS "AverageSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Avg_Int() + { + base.Avg_Int(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", AVG("e"."WorkExperience") OVER () AS "AverageWork" +FROM "Employees" AS "e" +"""); + } + + public override void Avg_Decimal_Int_Cast_Decimal() + { + base.Avg_Decimal_Int_Cast_Decimal(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", AVG(CAST("e"."WorkExperience" AS TEXT)) OVER () AS "AverageWork" +FROM "Employees" AS "e" +"""); + } + + public override void Avg_Null() + { + base.Avg_Null(); + + AssertSql( + """ +SELECT "n"."Id", "n"."Name", AVG("n"."Salary") OVER () AS "AverageSalary" +FROM "NullTestEmployees" AS "n" +"""); + } + + public override void Avg_Filter() + { + base.Avg_Filter(); + + AssertSql( + """ +@__ids_1='[1,2,3]' (Size = 7) + +SELECT "e"."Id", "e"."Name", AVG(CASE + WHEN "e"."EmployeeId" IN ( + SELECT "i"."value" + FROM json_each(@__ids_1) AS "i" + ) THEN "e"."Salary" + ELSE NULL +END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Avg" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Sum Tests + + public override void Sum_Decimal() + { + base.Sum_Decimal(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", SUM("e"."Salary") OVER () AS "SumSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Sum_Int() + { + base.Sum_Int(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", SUM("e"."WorkExperience") OVER () AS "SumWorkExperience" +FROM "Employees" AS "e" +"""); + } + + public override void Sum_Null() + { + base.Sum_Null(); + + AssertSql( + """ +SELECT "n"."Id", "n"."Name", SUM("n"."Salary") OVER () AS "Sum" +FROM "NullTestEmployees" AS "n" +"""); + } + + public override void Sum_Filter() + { + base.Sum_Filter(); + + AssertSql( + """ +@__ids_1='[1,2,3]' (Size = 7) + +SELECT "e"."Id", "e"."Name", SUM(CASE + WHEN "e"."EmployeeId" IN ( + SELECT "i"."value" + FROM json_each(@__ids_1) AS "i" + ) THEN "e"."Salary" + ELSE NULL +END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Sum" +FROM "Employees" AS "e" +"""); + } + + #endregion + + public override void RowNumber_Basic() + { + base.RowNumber_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", ROW_NUMBER() OVER ( ORDER BY "e"."Name") AS "RowNumber" +FROM "Employees" AS "e" +"""); + } + + public override void First_Value_OderByEnd_Basic() + { + base.First_Value_OderByEnd_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", FIRST_VALUE("e"."Name") OVER ( ORDER BY "e"."Salary") AS "FirstValue" +FROM "Employees" AS "e" +"""); + } + + public override void First_Value_FrameEnd_Basic() + { + base.First_Value_FrameEnd_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", FIRST_VALUE("e"."Name") OVER ( ORDER BY "e"."Salary" ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS "FirstValue" +FROM "Employees" AS "e" +"""); + } + + public override void First_Value_Null() + { + base.First_Value_Null(); + + AssertSql( + """ +SELECT "n"."Id", "n"."Name", FIRST_VALUE("n"."Salary") OVER ( ORDER BY "n"."WorkExperience") AS "FirstValue" +FROM "NullTestEmployees" AS "n" +"""); + } + + public override void Last_Value_OderByEnd_Basic() + { + base.Last_Value_OderByEnd_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", LAST_VALUE("e"."Name") OVER ( ORDER BY "e"."Salary") AS "LastValue" +FROM "Employees" AS "e" +"""); + } + + public override void Last_Value_FrameEnd_Basic() + { + base.Last_Value_FrameEnd_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", LAST_VALUE("e"."Name") OVER ( ORDER BY "e"."Salary" ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS "LastValue" +FROM "Employees" AS "e" +"""); + } + + public override void Last_Value_Null() + { + base.Last_Value_Null(); + + AssertSql( + """ +SELECT "n"."Id", "n"."Name", LAST_VALUE("n"."Salary") OVER ( ORDER BY "n"."WorkExperience") AS "LastValue" +FROM "NullTestEmployees" AS "n" +"""); + } + + public override void Rank_Basic() + { + base.Rank_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", RANK() OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."WorkExperience") AS "Rank" +FROM "Employees" AS "e" +"""); + } + + public override void Dense_Rank_Basic() + { + base.Dense_Rank_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", DENSE_RANK() OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."WorkExperience") AS "Rank" +FROM "Employees" AS "e" +"""); + } + + public override void NTile_Basic() + { + base.NTile_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", NTILE(3) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."WorkExperience") AS "Rank" +FROM "Employees" AS "e" +"""); + } + + public override void Percent_Rank_Basic() + { + base.Percent_Rank_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", PERCENT_RANK() OVER ( ORDER BY "e"."Salary") AS "PercentRank" +FROM "Employees" AS "e" +"""); + } + + public override void Cume_Dist_Basic() + { + base.Cume_Dist_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", CUME_DIST() OVER ( ORDER BY "e"."Salary") AS "CumeDist" +FROM "Employees" AS "e" +"""); + } + + public override void Lag_Decimal_Basic() + { + base.Lag_Decimal_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", LAG("e"."Salary", 1, '0.0') OVER ( ORDER BY "e"."Salary") AS "PreviousSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Lag_Int_Basic() + { + base.Lag_Int_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", LAG("e"."Id", 1, 0) OVER ( ORDER BY "e"."Id") AS "PreviousId" +FROM "Employees" AS "e" +"""); + } + + public override void Lead_Decimal_Basic() + { + base.Lead_Decimal_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", LEAD("e"."Salary", 1, '0.0') OVER ( ORDER BY "e"."Salary") AS "NextSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Lead_Int_Basic() + { + base.Lead_Int_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", LEAD("e"."Id", 1, 0) OVER ( ORDER BY "e"."Id") AS "NextId" +FROM "Employees" AS "e" +"""); + } + + public override void Lag_String_Basic() + { + base.Lag_String_Basic(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", LAG("e"."Name", 1, 'test') OVER ( ORDER BY "e"."Name") AS "PreviousName" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region WindowOverExpression Equality tests + + public override void Multiple_Aggregates_Basic_NoDup_Query() + { + base.Multiple_Aggregates_Basic_NoDup_Query(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER () AS "MaxSalary", MIN("e"."Salary") OVER () AS "MinSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Multiple_Aggregates_Basic_Dup_Query() + { + base.Multiple_Aggregates_Basic_Dup_Query(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER () AS "MaxSalary1" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Rows / Range Tests + + #region Rows(int preceding) + + public override void Rows_Preceding_X() + { + base.Rows_Preceding_X(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS 2 PRECEDING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Rows(RowsPreceding preceding) + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow() + { + base.Rows_Preceding_CurrentRow(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding() + { + base.Rows_Preceding_UnboundedPreceding(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS UNBOUNDED PRECEDING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Rows(int preceding, int following) + + [ConditionalFact] + public override void Rows_Preceding_X_Following_X() + { + base.Rows_Preceding_X_Following_X(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Rows(RowsPreceding preceding, int following) + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow_Following_X() + { + base.Rows_Preceding_CurrentRow_Following_X(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding_Following_X() + { + base.Rows_Preceding_UnboundedPreceding_Following_X(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN UNBOUNDED PRECEDING AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Rows(int preceding, RowsFollowing following) + + [ConditionalFact] + public override void Rows_Preceding_X_Following_CurrentRow() + { + base.Rows_Preceding_X_Following_CurrentRow(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_X_Following_UnboundedFollowing() + { + base.Rows_Preceding_X_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN 2 PRECEDING AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Rows(RowsPreceding preceding, RowsFollowing following) + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow_Following_CurrentRow() + { + base.Rows_Preceding_CurrentRow_Following_CurrentRow(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN CURRENT ROW AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_CurrentRow_Following_UnboundedFollowing() + { + base.Rows_Preceding_CurrentRow_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding_Following_CurrentRow() + { + base.Rows_Preceding_UnboundedPreceding_Following_CurrentRow(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Rows_Preceding_UnboundedPreceding_Following_UnboundedFollowing() + { + base.Rows_Preceding_UnboundedPreceding_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion Range(RowsPreceding preceding) + + #region Range(RowsPreceding preceding) + + [ConditionalFact] + public override void Range_CurrentRow() + { + base.Range_CurrentRow(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Range_UnboundedPreceding() + { + base.Range_UnboundedPreceding(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE UNBOUNDED PRECEDING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Range(RowsPreceding preceding, RowsFollowing following) + + [ConditionalFact] + public override void Range_Preceding_CurrentRow_Following_CurrentRow() + { + base.Range_Preceding_CurrentRow_Following_CurrentRow(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN CURRENT ROW AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Range_Preceding_CurrentRow_Following_UnboundedFollowing() + { + base.Range_Preceding_CurrentRow_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Range_Preceding_UnboundedPreceding_Following_CurrentRow() + { + base.Range_Preceding_UnboundedPreceding_Following_CurrentRow(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public override void Range_Preceding_UnboundedPreceding_Following_UnboundedFollowing() + { + base.Range_Preceding_UnboundedPreceding_Following_UnboundedFollowing(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + + #endregion + + } + + #endregion + + #region Partition / Order By + + public override void Rows_No_Parition() + { + base.Rows_No_Parition(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER ( ORDER BY "e"."Name" ROWS BETWEEN 1 PRECEDING AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Range_No_Parition() + { + base.Range_No_Parition(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER ( ORDER BY "e"."Name" RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +ORDER BY "e"."Name" +"""); + } + + public override void OrderBy_No_Parition() + { + base.OrderBy_No_Parition(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER ( ORDER BY "e"."Name") AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void OrderBy_Desc_No_Parition() + { + base.OrderBy_Desc_No_Parition(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER ( ORDER BY "e"."Name" DESC) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void OrderBy_Desc_Rows() + { + base.OrderBy_Desc_Rows(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER ( ORDER BY "e"."Name" DESC ROWS 1 PRECEDING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Outer_Order_By_Sql() + { + base.Outer_Order_By_Sql(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", RANK() OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."WorkExperience", "e"."Name") AS "Rank" +FROM "Employees" AS "e" +ORDER BY "e"."Name" +"""); + } + + public override void Partition_No_OrderBy_No_Frame() + { + base.Partition_No_OrderBy_No_Frame(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName") AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Partition_MultipleColumns() + { + base.Partition_MultipleColumns(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName", "e"."WorkExperience") AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + public override void Partition_ColumnModified() + { + base.Partition_ColumnModified(); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."WorkExperience" / 10) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region SQLLite Specific + + #region Range(int preceding, int following) + + [ConditionalFact] + public virtual void Range_Preceding_X_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(1, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN 1 PRECEDING AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Range(RowsPreceding preceding, int following) + + [ConditionalFact] + public virtual void Range_Preceding_CurrentRow_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.CurrentRow, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN CURRENT ROW AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Range_Preceding_UnboundedPreceding_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(RowsPreceding.UnboundedPreceding, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN UNBOUNDED PRECEDING AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Range(int preceding, RowsFollowing following) + + [ConditionalFact] + public virtual void Range_Preceding_X_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(2, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN 2 PRECEDING AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Range_Preceding_X_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Range(2, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" RANGE BETWEEN 2 PRECEDING AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Groups + + #region Groups(int preceding) + + [ConditionalFact] + public virtual void Groups_Preceding_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS 2 PRECEDING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Groups(GroupsPreceding preceding) + + [ConditionalFact] + public virtual void Groups_Preceding_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Groups_Preceding_UnboundedPreceding() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.UnboundedPreceding).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS UNBOUNDED PRECEDING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Groups(int preceding, int following) + + [ConditionalFact] + public virtual void Groups_Preceding_X_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(1, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN 1 PRECEDING AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Groups(GroupsPreceding preceding, int following) + + [ConditionalFact] + public virtual void Groups_Preceding_CurrentRow_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.CurrentRow, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN CURRENT ROW AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Groups_Preceding_UnboundedPreceding_Following_X() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.UnboundedPreceding, 2).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN UNBOUNDED PRECEDING AND 2 FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Groups(int preceding, GroupsFollowing following) + + [ConditionalFact] + public virtual void Groups_Preceding_X_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Groups_Preceding_X_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(2, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN 2 PRECEDING AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #region Groups(GroupsPreceding preceding, GroupsFollowing following) + + [ConditionalFact] + public virtual void Groups_Preceding_CurrentRow_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.CurrentRow, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN CURRENT ROW AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Groups_Preceding_CurrentRow_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.CurrentRow, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Groups_Preceding_UnboundedPreceding_Following_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.UnboundedPreceding, RowsFollowing.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Groups_Preceding_UnboundedPreceding_Following_UnboundedFollowing() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Groups(RowsPreceding.UnboundedPreceding, RowsFollowing.UnboundedFollowing).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( +""" +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #endregion + + #region Exclude + + [ConditionalFact] + public virtual void Exclude_NoOthers() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2).Exclude(FrameExclude.NoOthers).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS 2 PRECEDING EXCLUDE NO OTHERS) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Exclude_CurrentRow() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2).Exclude(FrameExclude.CurrentRow).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Null(results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS 2 PRECEDING EXCLUDE CURRENT ROW) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Exclude_Group() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2).Exclude(FrameExclude.Group).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Null(results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS 2 PRECEDING EXCLUDE GROUP) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + [ConditionalFact] + public virtual void Exclude_Ties() + { + using var context = CreateContext(); + + var results = context.Employees.Select(e => new + { + e.Id, + e.Name, + MaxSalary = EF.Functions.Over().PartitionBy(e.DepartmentName).OrderBy(e.Name).Rows(2).Exclude(FrameExclude.Ties).Max(e.Salary) + }).ToList(); + + Assert.Equal(8, results.Count); + Assert.Equal(1000000.53m, results[0].MaxSalary); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name", MAX("e"."Salary") OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name" ROWS 2 PRECEDING EXCLUDE TIES) AS "MaxSalary" +FROM "Employees" AS "e" +"""); + } + + #endregion + + #endregion + + #region Where + + #endregion + + #region Order By + + public override void WindowFunctionInOrderBy() + { + base.WindowFunctionInOrderBy(); + + AssertSql( + """ +SELECT "e"."Id", "e"."Name" +FROM "Employees" AS "e" +ORDER BY ROW_NUMBER() OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") +"""); + } + + #endregion + + #endregion + + public void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} From 90de690a2bd2270a8b10ca54922bac82b364ff43 Mon Sep 17 00:00:00 2001 From: Paul Middleton Date: Sat, 20 Jul 2024 15:02:49 -0500 Subject: [PATCH 2/3] wip --- ...ntityFrameworkRelationalServicesBuilder.cs | 3 +- .../WindowBuilderExpressionFactory.cs | 2 +- ...lationalWindowAggregateMethodTranslator.cs | 64 +++++++++++-------- ...owAggregateMethodTranslatorDependencies.cs | 31 +++++++++ ...elationalWindowBuilderExpressionFactory.cs | 8 +-- .../SqlExpressions/WindowFrameExpression.cs | 6 +- .../SqlExpressions/WindowOverExpression.cs | 10 +-- .../WindowPartitionExpression.cs | 2 +- .../SqlServerServiceCollectionExtensions.cs | 1 - ...qlServerWindowAggregateMethodTranslator.cs | 34 +++++----- .../Query/WindowFunctionSqlServerTest.cs | 12 ---- .../Query/WindowFunctionSqliteTest.cs | 6 -- 12 files changed, 98 insertions(+), 81 deletions(-) rename src/EFCore.Relational/Query/{ => Internal}/WindowBuilderExpressionFactory.cs (94%) create mode 100644 src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslatorDependencies.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index ea6aac44893..0d15ad06611 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -234,7 +234,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() - .AddDependencyScoped(); + .AddDependencyScoped() + .AddDependencyScoped(); return base.TryAddCoreServices(); } diff --git a/src/EFCore.Relational/Query/WindowBuilderExpressionFactory.cs b/src/EFCore.Relational/Query/Internal/WindowBuilderExpressionFactory.cs similarity index 94% rename from src/EFCore.Relational/Query/WindowBuilderExpressionFactory.cs rename to src/EFCore.Relational/Query/Internal/WindowBuilderExpressionFactory.cs index 39f37479f09..38414d071c1 100644 --- a/src/EFCore.Relational/Query/WindowBuilderExpressionFactory.cs +++ b/src/EFCore.Relational/Query/Internal/WindowBuilderExpressionFactory.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace Microsoft.EntityFrameworkCore.Query; +namespace Microsoft.EntityFrameworkCore.Query.Internal; /// /// todo diff --git a/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs b/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs index ce9207f5a01..b6aabb65caa 100644 --- a/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs +++ b/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslator.cs @@ -18,7 +18,15 @@ namespace Microsoft.EntityFrameworkCore.Query; /// public class RelationalWindowAggregateMethodTranslator : IWindowAggregateMethodCallTranslator { - private readonly ISqlExpressionFactory _sqlExpressionFactory; + /// + /// todo + /// + public virtual ISqlExpressionFactory SqlExpressionFactory => Dependencies.SqlExpressionFactory; + + /// + /// todo + /// + public virtual RelationalWindowAggregateMethodTranslatorDependencies Dependencies { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -26,9 +34,9 @@ public class RelationalWindowAggregateMethodTranslator : IWindowAggregateMethodC /// 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 RelationalWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + public RelationalWindowAggregateMethodTranslator(RelationalWindowAggregateMethodTranslatorDependencies dependencies) { - _sqlExpressionFactory = sqlExpressionFactory; + Dependencies = dependencies; } /// @@ -50,42 +58,42 @@ public RelationalWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpres case nameof(RelationalWindowAggregateFunctionExtensions.Average) when methodInfo == RelationalWindowAggregateMethods.Average: - return _sqlExpressionFactory.Function("AVG", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("AVG", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Average) when methodInfo == RelationalWindowAggregateMethods.AverageFilter: - return _sqlExpressionFactory.Function("AVG", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("AVG", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Count) when methodInfo == RelationalWindowAggregateMethods.CountAll: - return _sqlExpressionFactory.Function("COUNT", [_sqlExpressionFactory.Fragment("*")], false, [false], typeof(int)); + return SqlExpressionFactory.Function("COUNT", [SqlExpressionFactory.Fragment("*")], false, [false], typeof(int)); case nameof(RelationalWindowAggregateFunctionExtensions.Count) when methodInfo == RelationalWindowAggregateMethods.CountAllFilter: - return _sqlExpressionFactory.Function("COUNT", BuildCaseExpression(arguments, _sqlExpressionFactory.Constant("1")), true, [false], typeof(int)); + return SqlExpressionFactory.Function("COUNT", BuildCaseExpression(arguments, SqlExpressionFactory.Constant("1")), true, [false], typeof(int)); case nameof(RelationalWindowAggregateFunctionExtensions.Count) when methodInfo == RelationalWindowAggregateMethods.CountCol: - return _sqlExpressionFactory.Function("COUNT", arguments, false, [false], typeof(int)); + return SqlExpressionFactory.Function("COUNT", arguments, false, [false], typeof(int)); case nameof(RelationalWindowAggregateFunctionExtensions.Count) when methodInfo == RelationalWindowAggregateMethods.CountColFilter: - return _sqlExpressionFactory.Function("COUNT", BuildCaseExpression(arguments), true, [false], typeof(int)); + return SqlExpressionFactory.Function("COUNT", BuildCaseExpression(arguments), true, [false], typeof(int)); case nameof(RelationalWindowAggregateFunctionExtensions.CumeDist) when methodInfo == RelationalWindowAggregateMethods.CumeDist: - return _sqlExpressionFactory.Function("CUME_DIST", Enumerable.Empty(), false, [], typeof(double)); + return SqlExpressionFactory.Function("CUME_DIST", Enumerable.Empty(), false, [], typeof(double)); case nameof(RelationalWindowAggregateFunctionExtensions.DenseRank) when methodInfo == RelationalWindowAggregateMethods.DenseRank: - return _sqlExpressionFactory.Function("DENSE_RANK", Enumerable.Empty(), false, [], typeof(long)); + return SqlExpressionFactory.Function("DENSE_RANK", Enumerable.Empty(), false, [], typeof(long)); case nameof(RelationalWindowAggregateFunctionExtensions.FirstValue) when methodInfo == RelationalWindowAggregateMethods.FirstValueFrameResults: @@ -93,12 +101,12 @@ public RelationalWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpres case nameof(RelationalWindowAggregateFunctionExtensions.FirstValue) when methodInfo == RelationalWindowAggregateMethods.FirstValueOrderThen: - return _sqlExpressionFactory.Function("FIRST_VALUE", arguments, true, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("FIRST_VALUE", arguments, true, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Lag) when methodInfo == RelationalWindowAggregateMethods.Lag: - return _sqlExpressionFactory.Function("LAG", arguments, true, [false, false, false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("LAG", arguments, true, [false, false, false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.LastValue) when methodInfo == RelationalWindowAggregateMethods.LastValueOrderThen: @@ -106,62 +114,62 @@ public RelationalWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpres case nameof(RelationalWindowAggregateFunctionExtensions.LastValue) when methodInfo == RelationalWindowAggregateMethods.LastValueFrameResults: - return _sqlExpressionFactory.Function("LAST_VALUE", arguments, true, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("LAST_VALUE", arguments, true, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Lead) when methodInfo == RelationalWindowAggregateMethods.Lead: - return _sqlExpressionFactory.Function("LEAD", arguments, true, [false, false, false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("LEAD", arguments, true, [false, false, false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Max) when methodInfo == RelationalWindowAggregateMethods.Max: - return _sqlExpressionFactory.Function("MAX", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("MAX", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Max) when methodInfo == RelationalWindowAggregateMethods.MaxFilter: - return _sqlExpressionFactory.Function("MAX", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("MAX", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Min) when methodInfo == RelationalWindowAggregateMethods.Min: - return _sqlExpressionFactory.Function("MIN", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("MIN", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Min) when methodInfo == RelationalWindowAggregateMethods.MinFilter: - return _sqlExpressionFactory.Function("MIN", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("MIN", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.NTile) when methodInfo == RelationalWindowAggregateMethods.NTile: - return _sqlExpressionFactory.Function("NTILE", arguments, false, [false], typeof(long)); + return SqlExpressionFactory.Function("NTILE", arguments, false, [false], typeof(long)); case nameof(RelationalWindowAggregateFunctionExtensions.PercentRank) when methodInfo == RelationalWindowAggregateMethods.PercentRank: - return _sqlExpressionFactory.Function("PERCENT_RANK", Enumerable.Empty(), false, [], typeof(double)); + return SqlExpressionFactory.Function("PERCENT_RANK", Enumerable.Empty(), false, [], typeof(double)); case nameof(RelationalWindowAggregateFunctionExtensions.Rank) when methodInfo == RelationalWindowAggregateMethods.Rank: - return _sqlExpressionFactory.Function("RANK", Enumerable.Empty(), false, [], typeof(long)); + return SqlExpressionFactory.Function("RANK", Enumerable.Empty(), false, [], typeof(long)); case nameof(RelationalWindowAggregateFunctionExtensions.RowNumber) when methodInfo == RelationalWindowAggregateMethods.RowNumber: - return _sqlExpressionFactory.Function("ROW_NUMBER", Enumerable.Empty(), false, [], typeof(long)); + return SqlExpressionFactory.Function("ROW_NUMBER", Enumerable.Empty(), false, [], typeof(long)); case nameof(RelationalWindowAggregateFunctionExtensions.Sum) when methodInfo == RelationalWindowAggregateMethods.Sum: - return _sqlExpressionFactory.Function("SUM", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("SUM", arguments, false, [false], arguments[0].Type, arguments[0].TypeMapping); case nameof(RelationalWindowAggregateFunctionExtensions.Sum) when methodInfo == RelationalWindowAggregateMethods.SumFilter: - return _sqlExpressionFactory.Function("SUM", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); + return SqlExpressionFactory.Function("SUM", BuildCaseExpression(arguments), true, [false], arguments[0].Type, arguments[0].TypeMapping); } return null; @@ -174,7 +182,7 @@ public RelationalWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpres /// todo /// todo protected virtual SqlExpression[] BuildCaseExpression(IReadOnlyList arguments, SqlExpression? result = null) - => [_sqlExpressionFactory.Case([new CaseWhenClause(ProcessCaseWhen(arguments[result == null ? 1 : 0]), result ?? arguments[0])], _sqlExpressionFactory.Constant(null, typeof(object)))] ; + => [SqlExpressionFactory.Case([new CaseWhenClause(ProcessCaseWhen(arguments[result == null ? 1 : 0]), result ?? arguments[0])], SqlExpressionFactory.Constant(null, typeof(object)))]; /// @@ -184,11 +192,11 @@ protected virtual SqlExpression[] BuildCaseExpression(IReadOnlyListtodo protected virtual SqlExpression ProcessCaseWhen(SqlExpression whenExpression) { - if(whenExpression is SqlBinaryExpression { Left : InExpression inExpression, Right : SqlConstantExpression constantExpression }) + if (whenExpression is SqlBinaryExpression { Left: InExpression inExpression, Right: SqlConstantExpression constantExpression }) { return constantExpression.Value as bool? == true ? inExpression - : _sqlExpressionFactory.Not(inExpression); + : SqlExpressionFactory.Not(inExpression); } return whenExpression; diff --git a/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslatorDependencies.cs b/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslatorDependencies.cs new file mode 100644 index 00000000000..ba7b310c69c --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalWindowAggregateMethodTranslatorDependencies.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// todo + /// + public record RelationalWindowAggregateMethodTranslatorDependencies + { + /// + /// todo + /// + /// todo + public RelationalWindowAggregateMethodTranslatorDependencies(ISqlExpressionFactory sqlExpressionFactory) + { + SqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// todo + /// + public virtual ISqlExpressionFactory SqlExpressionFactory { get; } + } +} diff --git a/src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs b/src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs index 48ea472f4bb..76aac8b3d06 100644 --- a/src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs +++ b/src/EFCore.Relational/Query/RelationalWindowBuilderExpressionFactory.cs @@ -35,22 +35,22 @@ public RelationalWindowBuilderExpression(ISqlExpressionFactory sqlExpressionFact /// /// todo /// - public IReadOnlyList OrderingExpressions => _orderingExpressions; + public virtual IReadOnlyList OrderingExpressions => _orderingExpressions; /// /// todo /// - public WindowPartitionExpression? PartitionExpression => _partitionExpression; + public virtual WindowPartitionExpression? PartitionExpression => _partitionExpression; /// /// todo /// - public WindowFrameExpression? FrameExpression => _frameExpression; + public virtual WindowFrameExpression? FrameExpression => _frameExpression; /// /// todo /// - public SqlConstantExpression? ExcludeExpression => _excludeExpression; + public virtual SqlConstantExpression? ExcludeExpression => _excludeExpression; /// /// todo diff --git a/src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs index 1843e53dc6d..4141b61943e 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/WindowFrameExpression.cs @@ -17,12 +17,12 @@ public abstract class WindowFrameExpression : Expression, IPrintableExpression /// /// todo /// - public SqlExpression? Preceding { get; init; } + public virtual SqlExpression? Preceding { get; init; } /// /// todo /// - public SqlExpression? Following { get; init; } + public virtual SqlExpression? Following { get; init; } /// /// todo - bettter name @@ -32,7 +32,7 @@ public abstract class WindowFrameExpression : Expression, IPrintableExpression /// /// todo /// - public SqlExpression? Exclude { get; set; } + public virtual SqlExpression? Exclude { get; set; } /// /// todo diff --git a/src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs index e9858866173..d72cd8cfc63 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/WindowOverExpression.cs @@ -20,27 +20,27 @@ public class WindowOverExpression : SqlExpression, IPrintableExpression /// /// todo /// - public WindowPartitionExpression? Partition { get; init; } + public virtual WindowPartitionExpression? Partition { get; init; } /// /// todo /// - public SqlFunctionExpression Aggregate { get; set; } + public virtual SqlFunctionExpression Aggregate { get; set; } /// /// todo /// - public IReadOnlyList Ordering { get; init; } + public virtual IReadOnlyList Ordering { get; init; } /// /// todo /// - public WindowFrameExpression? WindowFrame { get; init; } + public virtual WindowFrameExpression? WindowFrame { get; init; } /// /// todo /// - public SqlExpression? Filter { get; init; } + public virtual SqlExpression? Filter { get; init; } /// diff --git a/src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs index b2c785f17f4..eb3f9eecfd2 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/WindowPartitionExpression.cs @@ -18,7 +18,7 @@ public class WindowPartitionExpression : Expression, IPrintableExpression /// /// todo /// - public IReadOnlyList Partitions { get; init; } + public virtual IReadOnlyList Partitions { get; init; } /// /// tests diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index cfda19068b7..786f5291bf0 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -269,7 +269,6 @@ private static IServiceCollection AddEntityFrameworkSqlEngine(IServiceCollection .TryAdd() .TryAdd() .TryAdd() - //.TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs index 92c3ee2da61..6ec89042a26 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerWindowAggregateMethodTranslator.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; @@ -16,16 +15,13 @@ namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; /// public class SqlServerWindowAggregateMethodTranslator : RelationalWindowAggregateMethodTranslator { - private readonly ISqlExpressionFactory _sqlExpressionFactory; - /// /// todo /// - /// todo - public SqlServerWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) - : base(sqlExpressionFactory) + /// todo + public SqlServerWindowAggregateMethodTranslator(RelationalWindowAggregateMethodTranslatorDependencies dependencies) + : base(dependencies) { - _sqlExpressionFactory = sqlExpressionFactory; } /// @@ -51,62 +47,62 @@ public SqlServerWindowAggregateMethodTranslator(ISqlExpressionFactory sqlExpress case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) when methodInfo == SqlServerWindowAggregateMethods.CountBigAll: - return _sqlExpressionFactory.Function("COUNT_BIG", new[] { _sqlExpressionFactory.Fragment("*") }, false, [false], typeof(long)); + return SqlExpressionFactory.Function("COUNT_BIG", new[] { SqlExpressionFactory.Fragment("*") }, false, [false], typeof(long)); case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) when methodInfo == SqlServerWindowAggregateMethods.CountBigAllFilter: - return _sqlExpressionFactory.Function("COUNT_BIG", BuildCaseExpression(arguments, _sqlExpressionFactory.Constant("1")), true, [false], typeof(long)); + return SqlExpressionFactory.Function("COUNT_BIG", BuildCaseExpression(arguments, SqlExpressionFactory.Constant("1")), true, [false], typeof(long)); case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) when methodInfo == SqlServerWindowAggregateMethods.CountBigCol: - return _sqlExpressionFactory.Function("COUNT_BIG", arguments, false, [false], typeof(long)); + return SqlExpressionFactory.Function("COUNT_BIG", arguments, false, [false], typeof(long)); case nameof(SqlServerWindowAggregateFunctionExtensions.CountBig) when methodInfo == SqlServerWindowAggregateMethods.CountBigColFilter: - return _sqlExpressionFactory.Function("COUNT_BIG", BuildCaseExpression(arguments), true, [false], typeof(long)); + return SqlExpressionFactory.Function("COUNT_BIG", BuildCaseExpression(arguments), true, [false], typeof(long)); case nameof(SqlServerWindowAggregateFunctionExtensions.Stdev) when methodInfo == SqlServerWindowAggregateMethods.Stdev: - return _sqlExpressionFactory.Function("STDEV", arguments, true, [false], typeof(double)); + return SqlExpressionFactory.Function("STDEV", arguments, true, [false], typeof(double)); case nameof(SqlServerWindowAggregateFunctionExtensions.Stdev) when methodInfo == SqlServerWindowAggregateMethods.StdevFilter: - return _sqlExpressionFactory.Function("STDEV", BuildCaseExpression(arguments), true, [false], typeof(double)); + return SqlExpressionFactory.Function("STDEV", BuildCaseExpression(arguments), true, [false], typeof(double)); case nameof(SqlServerWindowAggregateFunctionExtensions.StdevP) when methodInfo == SqlServerWindowAggregateMethods.StdevP: - return _sqlExpressionFactory.Function("STDEVP", arguments, true, [false], typeof(double)); + return SqlExpressionFactory.Function("STDEVP", arguments, true, [false], typeof(double)); case nameof(SqlServerWindowAggregateFunctionExtensions.StdevP) when methodInfo == SqlServerWindowAggregateMethods.StdevPFilter: - return _sqlExpressionFactory.Function("STDEVP", BuildCaseExpression(arguments), true, [false], typeof(double)); + return SqlExpressionFactory.Function("STDEVP", BuildCaseExpression(arguments), true, [false], typeof(double)); case nameof(SqlServerWindowAggregateFunctionExtensions.Var) when methodInfo == SqlServerWindowAggregateMethods.Var: - return _sqlExpressionFactory.Function("VAR", arguments, true, [false], typeof(double)); + return SqlExpressionFactory.Function("VAR", arguments, true, [false], typeof(double)); case nameof(SqlServerWindowAggregateFunctionExtensions.Var) when methodInfo == SqlServerWindowAggregateMethods.VarFilter: - return _sqlExpressionFactory.Function("VAR", BuildCaseExpression(arguments), true, [false], typeof(double)); + return SqlExpressionFactory.Function("VAR", BuildCaseExpression(arguments), true, [false], typeof(double)); case nameof(SqlServerWindowAggregateFunctionExtensions.VarP) when methodInfo == SqlServerWindowAggregateMethods.VarP: - return _sqlExpressionFactory.Function("VARP", arguments, true, [false], typeof(double)); + return SqlExpressionFactory.Function("VARP", arguments, true, [false], typeof(double)); case nameof(SqlServerWindowAggregateFunctionExtensions.VarP) when methodInfo == SqlServerWindowAggregateMethods.VarPFilter: - return _sqlExpressionFactory.Function("VARP", BuildCaseExpression(arguments), true, [false], typeof(double)); + return SqlExpressionFactory.Function("VARP", BuildCaseExpression(arguments), true, [false], typeof(double)); } return null; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs index 31a1a0225ab..8cadace023c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs @@ -73,7 +73,6 @@ public override void Max_Filter() """ SELECT [e].[Id], [e].[Name], MAX(CASE WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] - ELSE NULL END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [MaxSalary] FROM [Employees] AS [e] """); @@ -113,7 +112,6 @@ public override void Min_Filter() """ SELECT [e].[Id], [e].[Name], MIN(CASE WHEN [e].[Salary] = 200000.0 THEN [e].[Salary] - ELSE NULL END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [MinSalary] FROM [Employees] AS [e] """); @@ -153,7 +151,6 @@ public override void Count_Star_Filter() """ SELECT [e].[Id], [e].[Name], COUNT(CASE WHEN [e].[Salary] <= 1200000.0 THEN N'1' - ELSE NULL END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Count] FROM [Employees] AS [e] """); @@ -167,7 +164,6 @@ public override void Count_Col_Filter() """ SELECT [e].[Id], [e].[Name], COUNT(CASE WHEN [e].[Salary] <> 500000.0 THEN [e].[Salary] - ELSE NULL END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Count] FROM [Employees] AS [e] """); @@ -234,7 +230,6 @@ WHEN [e].[EmployeeId] IN ( SELECT [i].[value] FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] ) THEN [e].[Salary] - ELSE NULL END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Avg] FROM [Employees] AS [e] """); @@ -290,7 +285,6 @@ WHEN [e].[EmployeeId] IN ( SELECT [i].[value] FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] ) THEN [e].[Salary] - ELSE NULL END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Sum] FROM [Employees] AS [e] """); @@ -578,7 +572,6 @@ public void Count_Big_Star_Filter() """ SELECT [e].[Id], [e].[Name], COUNT_BIG(CASE WHEN [e].[Salary] > 10.0 THEN N'1' - ELSE NULL END) OVER () AS [Count] FROM [Employees] AS [e] """); @@ -603,7 +596,6 @@ public void Count_Big_Col_Filter() """ SELECT [e].[Id], [e].[Name], COUNT_BIG(CASE WHEN [e].[Salary] > 10.0 THEN [e].[Id] - ELSE NULL END) OVER () AS [Count] FROM [Employees] AS [e] """); @@ -666,7 +658,6 @@ public void Stdev_Filter() """ SELECT [e].[Id], [e].[Name], STDEV(CASE WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] - ELSE NULL END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] FROM [Employees] AS [e] """); @@ -730,7 +721,6 @@ public void StdevP_Filter() """ SELECT [e].[Id], [e].[Name], STDEVP(CASE WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] - ELSE NULL END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] FROM [Employees] AS [e] """); @@ -793,7 +783,6 @@ public void Var_Filter() """ SELECT [e].[Id], [e].[Name], VAR(CASE WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] - ELSE NULL END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] FROM [Employees] AS [e] """); @@ -856,7 +845,6 @@ public void VarP_Filter() """ SELECT [e].[Id], [e].[Name], VARP(CASE WHEN [e].[Salary] > 100000.0 THEN [e].[Salary] - ELSE NULL END) OVER ( ORDER BY [e].[WorkExperience], [e].[Name]) AS [StdDev] FROM [Employees] AS [e] """); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs index f08280bd959..7b0bf1150d5 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/WindowFunctionSqliteTest.cs @@ -73,7 +73,6 @@ public override void Max_Filter() """ SELECT "e"."Id", "e"."Name", MAX(CASE WHEN ef_compare("e"."Salary", '100000.0') > 0 THEN "e"."Salary" - ELSE NULL END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "MaxSalary" FROM "Employees" AS "e" """); @@ -113,7 +112,6 @@ public override void Min_Filter() """ SELECT "e"."Id", "e"."Name", MIN(CASE WHEN "e"."Salary" = '200000.0' THEN "e"."Salary" - ELSE NULL END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "MinSalary" FROM "Employees" AS "e" """); @@ -153,7 +151,6 @@ public override void Count_Star_Filter() """ SELECT "e"."Id", "e"."Name", COUNT(CASE WHEN ef_compare("e"."Salary", '1200000.0') <= 0 THEN '1' - ELSE NULL END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Count" FROM "Employees" AS "e" """); @@ -167,7 +164,6 @@ public override void Count_Col_Filter() """ SELECT "e"."Id", "e"."Name", COUNT(CASE WHEN "e"."Salary" <> '500000.0' THEN "e"."Salary" - ELSE NULL END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Count" FROM "Employees" AS "e" """); @@ -234,7 +230,6 @@ public override void Avg_Filter() SELECT "i"."value" FROM json_each(@__ids_1) AS "i" ) THEN "e"."Salary" - ELSE NULL END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Avg" FROM "Employees" AS "e" """); @@ -290,7 +285,6 @@ public override void Sum_Filter() SELECT "i"."value" FROM json_each(@__ids_1) AS "i" ) THEN "e"."Salary" - ELSE NULL END) OVER (PARTITION BY "e"."DepartmentName" ORDER BY "e"."Name") AS "Sum" FROM "Employees" AS "e" """); From 15e2756b8ab1c35d82281f955fefa8da37e8ffae Mon Sep 17 00:00:00 2001 From: Paul Middleton Date: Mon, 25 Nov 2024 22:35:49 -0600 Subject: [PATCH 3/3] adjust for mainline rebase --- .../Query/WindowFunctionSqlServerTest.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs index 8cadace023c..f892a4b77d0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/WindowFunctionSqlServerTest.cs @@ -225,13 +225,16 @@ public override void Avg_Filter() """ @__ids_1='[1,2,3]' (Size = 4000) -SELECT [e].[Id], [e].[Name], AVG(CASE - WHEN [e].[EmployeeId] IN ( - SELECT [i].[value] - FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] - ) THEN [e].[Salary] -END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Avg] +SELECT [e].[Id], [e].[Name], AVG([s].[value]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Avg] FROM [Employees] AS [e] +OUTER APPLY ( + SELECT CASE + WHEN [e].[EmployeeId] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] + ) THEN [e].[Salary] + END AS [value] +) AS [s] """); } @@ -280,13 +283,16 @@ public override void Sum_Filter() """ @__ids_1='[1,2,3]' (Size = 4000) -SELECT [e].[Id], [e].[Name], SUM(CASE - WHEN [e].[EmployeeId] IN ( - SELECT [i].[value] - FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] - ) THEN [e].[Salary] -END) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Sum] +SELECT [e].[Id], [e].[Name], SUM([s].[value]) OVER (PARTITION BY [e].[DepartmentName] ORDER BY [e].[Name]) AS [Sum] FROM [Employees] AS [e] +OUTER APPLY ( + SELECT CASE + WHEN [e].[EmployeeId] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ids_1) WITH ([value] int '$') AS [i] + ) THEN [e].[Salary] + END AS [value] +) AS [s] """); }