Skip to content

Commit 354adda

Browse files
committed
WIP
1 parent 594688b commit 354adda

32 files changed

+6541
-7
lines changed

src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public static readonly IDictionary<Type, ServiceCharacteristics> RelationalServi
6969
{ typeof(IRelationalSqlTranslatingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
7070
{ typeof(IMethodCallTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Scoped) },
7171
{ typeof(IAggregateMethodCallTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Scoped) },
72+
{ typeof(IWindowAggregateMethodCallTranslator), new ServiceCharacteristics(ServiceLifetime.Scoped) },
7273
{ typeof(IMemberTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Scoped) },
7374
{ typeof(ISqlExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
7475
{ typeof(IRelationalQueryStringFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
@@ -96,7 +97,8 @@ public static readonly IDictionary<Type, ServiceCharacteristics> RelationalServi
9697
typeof(IAggregateMethodCallTranslatorPlugin),
9798
new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true)
9899
},
99-
{ typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }
100+
{ typeof(IMemberTranslatorPlugin), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) },
101+
{ typeof(IWindowBuilderExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }
100102
};
101103

102104
/// <summary>
@@ -179,6 +181,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
179181
TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, RelationalQueryableMethodTranslatingExpressionVisitorFactory>();
180182
TryAdd<IMethodCallTranslatorProvider, RelationalMethodCallTranslatorProvider>();
181183
TryAdd<IAggregateMethodCallTranslatorProvider, RelationalAggregateMethodCallTranslatorProvider>();
184+
TryAdd<IWindowAggregateMethodCallTranslator, RelationalWindowAggregateMethodTranslator>();
182185
TryAdd<IMemberTranslatorProvider, RelationalMemberTranslatorProvider>();
183186
TryAdd<IQueryTranslationPostprocessorFactory, RelationalQueryTranslationPostprocessorFactory>();
184187
TryAdd<IRelationalSqlTranslatingExpressionVisitorFactory, RelationalSqlTranslatingExpressionVisitorFactory>();
@@ -192,6 +195,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
192195
TryAdd<ILiftableConstantFactory>(p => p.GetRequiredService<IRelationalLiftableConstantFactory>());
193196
TryAdd<IRelationalLiftableConstantFactory, RelationalLiftableConstantFactory>();
194197
TryAdd<ILiftableConstantProcessor, RelationalLiftableConstantProcessor>();
198+
TryAdd<IWindowBuilderExpressionFactory, WindowBuilderExpressionFactory>();
195199

196200
ServiceCollectionMap.GetInfrastructure()
197201
.AddDependencySingleton<RelationalSqlGenerationHelperDependencies>()
@@ -229,7 +233,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
229233
.AddDependencyScoped<RelationalDatabaseDependencies>()
230234
.AddDependencyScoped<RelationalQueryContextDependencies>()
231235
.AddDependencyScoped<RelationalQueryCompilationContextDependencies>()
232-
.AddDependencyScoped<RelationalAdHocMapperDependencies>();
236+
.AddDependencyScoped<RelationalAdHocMapperDependencies>()
237+
.AddDependencyScoped<WindowBuilderExpressionFactory>();
233238

234239
return base.TryAddCoreServices();
235240
}

src/EFCore.Relational/Query/ISqlExpressionFactory.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,32 @@ bool TryCreateGreatest(
465465
IReadOnlyList<SqlExpression> expressions,
466466
Type resultType,
467467
[NotNullWhen(true)] out SqlExpression? greatestExpression);
468+
469+
/// <summary>
470+
/// todo
471+
/// </summary>
472+
/// <param name="partitions">todo</param>
473+
/// <returns>todo</returns>
474+
WindowPartitionExpression PartitionBy(IEnumerable<SqlExpression> partitions);
475+
476+
/// <summary>
477+
/// todo
478+
/// </summary>
479+
/// <param name="aggregate">todo</param>
480+
/// <param name="partition">todo</param>
481+
/// <param name="orderings">todo</param>
482+
/// <param name="frame">todo</param>
483+
/// <returns>todo</returns>
484+
WindowOverExpression Over(SqlFunctionExpression aggregate, WindowPartitionExpression? partition, IReadOnlyList<OrderingExpression> orderings,
485+
WindowFrameExpression? frame);
486+
487+
/// <summary>
488+
/// todo
489+
/// </summary>
490+
/// <param name="method">todo</param>
491+
/// <param name="preceding">todo</param>
492+
/// <param name="following">todo</param>
493+
/// <param name="exclude">todo</param>
494+
/// <returns>todo</returns>
495+
WindowFrameExpression WindowFrame(MethodInfo method, SqlExpression? preceding, SqlExpression? following, SqlExpression? exclude);
468496
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
10+
11+
namespace Microsoft.EntityFrameworkCore.Query;
12+
13+
/// <summary>
14+
/// todo
15+
/// </summary>
16+
public interface IWindowAggregateMethodCallTranslator
17+
{
18+
/// <summary>
19+
/// todo
20+
/// </summary>
21+
/// <param name="method">todo</param>
22+
/// <param name="arguments">todo</param>
23+
/// <param name="logger">todo</param>
24+
/// <returns>todo</returns>
25+
SqlExpression? Translate(
26+
MethodInfo method,
27+
IReadOnlyList<SqlExpression> arguments,
28+
IDiagnosticsLogger<DbLoggerCategory.Query> logger);
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
10+
namespace Microsoft.EntityFrameworkCore.Query;
11+
12+
/// <summary>
13+
/// todo
14+
/// </summary>
15+
public interface IWindowAggregateMethodCallTranslatorPlugin
16+
{
17+
/// <summary>
18+
/// Gets the method call translators.
19+
/// </summary>
20+
IEnumerable<IWindowAggregateMethodCallTranslator> Translators { get; }
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
10+
namespace Microsoft.EntityFrameworkCore.Query;
11+
12+
/// <summary>
13+
/// todo
14+
/// </summary>
15+
public interface IWindowBuilderExpressionFactory
16+
{
17+
/// <summary>
18+
/// todo
19+
/// </summary>
20+
/// <returns>todo</returns>
21+
RelationalWindowBuilderExpression CreateWindowBuilder();
22+
}

src/EFCore.Relational/Query/QuerySqlGenerator.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Reflection.Metadata;
45
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
56
using Microsoft.EntityFrameworkCore.Storage.Internal;
67

@@ -1756,4 +1757,97 @@ protected virtual bool TryGetOperatorInfo(SqlExpression expression, out int prec
17561757
(precedence, isAssociative) = (default, default);
17571758
return false;
17581759
}
1760+
1761+
/// <inheritdoc />
1762+
protected override Expression VisitOver(WindowOverExpression windowOverExpression)
1763+
{
1764+
Visit(windowOverExpression.Aggregate);
1765+
1766+
_relationalCommandBuilder.Append(" OVER (");
1767+
1768+
if(windowOverExpression.Partition != null)
1769+
VisitWindowPartition(windowOverExpression.Partition);
1770+
1771+
if (windowOverExpression.Ordering.Count > 0)
1772+
{
1773+
_relationalCommandBuilder.Append(" ORDER BY ");
1774+
1775+
GenerateList(windowOverExpression.Ordering, e => Visit(e));
1776+
}
1777+
1778+
if (windowOverExpression.WindowFrame != null)
1779+
VisitWindowFrame(windowOverExpression.WindowFrame);
1780+
1781+
_relationalCommandBuilder.Append(")");
1782+
1783+
return windowOverExpression;
1784+
}
1785+
1786+
/// <inheritdoc />
1787+
protected override Expression VisitWindowPartition(WindowPartitionExpression partitionExpression)
1788+
{
1789+
_relationalCommandBuilder.Append("PARTITION BY ");
1790+
1791+
GenerateList(partitionExpression.Partitions, e => Visit(e), sql => sql.Append(", "));
1792+
1793+
return partitionExpression;
1794+
}
1795+
1796+
/// <inheritdoc />
1797+
protected override Expression VisitWindowFrame(WindowFrameExpression windowsFrameExpression)
1798+
{
1799+
//todo - sqllite groups override test
1800+
1801+
_relationalCommandBuilder.Append($" {windowsFrameExpression.FrameName} ");
1802+
1803+
if(windowsFrameExpression.Following != null)
1804+
_relationalCommandBuilder.Append($"BETWEEN ");
1805+
1806+
if (windowsFrameExpression.Preceding is SqlConstantExpression preceedingExpression && preceedingExpression?.Type == typeof(RowsPreceding))
1807+
{
1808+
_relationalCommandBuilder.Append((RowsPreceding)preceedingExpression.Value! == RowsPreceding.CurrentRow
1809+
? "CURRENT ROW"
1810+
: "UNBOUNDED PRECEDING");
1811+
}
1812+
else
1813+
{
1814+
Visit(windowsFrameExpression.Preceding);
1815+
1816+
_relationalCommandBuilder.Append($" PRECEDING");
1817+
}
1818+
1819+
if(windowsFrameExpression.Following != null)
1820+
{
1821+
_relationalCommandBuilder.Append($" AND ");
1822+
1823+
if (windowsFrameExpression.Following is SqlConstantExpression followingExpression && followingExpression?.Type == typeof(RowsFollowing))
1824+
{
1825+
_relationalCommandBuilder.Append((RowsPreceding)followingExpression.Value! == RowsPreceding.CurrentRow
1826+
? "CURRENT ROW"
1827+
: "UNBOUNDED FOLLOWING");
1828+
}
1829+
else
1830+
{
1831+
Visit(windowsFrameExpression.Following);
1832+
1833+
_relationalCommandBuilder.Append($" FOLLOWING");
1834+
}
1835+
}
1836+
1837+
if (windowsFrameExpression.Exclude is SqlConstantExpression excludeExpression && excludeExpression?.Type == typeof(FrameExclude))
1838+
{
1839+
_relationalCommandBuilder.Append($" EXCLUDE ");
1840+
1841+
_relationalCommandBuilder.Append((FrameExclude)excludeExpression.Value! switch
1842+
{
1843+
FrameExclude.NoOthers => "NO OTHERS",
1844+
FrameExclude.CurrentRow => "CURRENT ROW",
1845+
FrameExclude.Group => "GROUP",
1846+
FrameExclude.Ties => "TIES",
1847+
_ => throw new ArgumentOutOfRangeException()
1848+
});
1849+
}
1850+
1851+
return windowsFrameExpression;
1852+
}
17591853
}

src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model
4545
return false;
4646
}
4747

48-
if (method.DeclaringType == typeof(RelationalDbFunctionsExtensions))
48+
if (method.DeclaringType == typeof(RelationalDbFunctionsExtensions) || method.DeclaringType == typeof(WindowFunctionsExtensions))
4949
{
5050
return false;
5151
}
52+
5253
}
5354

5455
return base.IsEvaluatableExpression(expression, model);

src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -989,10 +989,125 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
989989
? translatedAggregate
990990
: TranslateAsSubquery(methodCallExpression);
991991

992+
// match windowing functions
993+
case
994+
{
995+
Method.Name: nameof(WindowFunctionsExtensions.Over)
996+
} when method.DeclaringType == typeof(WindowFunctionsExtensions):
997+
{
998+
return Dependencies.WindowBuilderExpressionFactory.CreateWindowBuilder();
999+
}
1000+
1001+
//what can I put in this case?
1002+
case
1003+
{
1004+
1005+
} when arguments.Count > 0 && typeof(IWindowFinal).IsAssignableFrom(arguments[0].Type):
1006+
{
1007+
//create object to deal with all of these else/if cases for windowing functions?
1008+
//this is the aggregate. Do we need something better than WindowFunctionsExtensions - how will custom providers add specific aggs
1009+
//todo - how many args could there be? might have to loop this. hardcode for max for now
1010+
1011+
var aggregateParams = new SqlExpression[arguments.Count - 1];
1012+
1013+
for (var i = 1; i < arguments.Count; i++)
1014+
{
1015+
if (TranslationFailed(arguments[i], Visit(RemoveObjectConvert(arguments[i] is LambdaExpression lambda ? lambda.Body : arguments[i])), out var translatedValue))
1016+
{
1017+
return QueryCompilationContext.NotTranslatedExpression;
1018+
}
1019+
1020+
aggregateParams[i - 1] = translatedValue!;
1021+
}
1022+
1023+
var windowingFunction = Dependencies.WindowAggregateMethodCallTranslator.Translate(method, aggregateParams, _queryCompilationContext.Logger) as SqlFunctionExpression;
1024+
1025+
if (windowingFunction == null)
1026+
{
1027+
return QueryCompilationContext.NotTranslatedExpression;
1028+
}
1029+
1030+
var wbe = (RelationalWindowBuilderExpression)Visit(arguments[0]);
1031+
1032+
return _sqlExpressionFactory.Over(windowingFunction, wbe.PartitionExpression, wbe.OrderingExpressions, wbe.FrameExpression);
1033+
}
1034+
1035+
case
1036+
{
1037+
Method.Name: nameof(IOver.PartitionBy)
1038+
} when method.DeclaringType == typeof(IOver):
1039+
{
1040+
if (!(Visit(methodCallExpression.Object) is RelationalWindowBuilderExpression wbe))
1041+
return QueryCompilationContext.NotTranslatedExpression;
1042+
1043+
var partitions = (NewArrayExpression)arguments[0];
1044+
var translatedPartitions = new SqlExpression[partitions.Expressions.Count];
1045+
1046+
for (var i = 0; i < partitions.Expressions.Count; i++)
1047+
{
1048+
if (TranslationFailed(partitions.Expressions[i], Visit(partitions.Expressions[i]), out var translatedValue))
1049+
{
1050+
return QueryCompilationContext.NotTranslatedExpression;
1051+
}
1052+
1053+
translatedPartitions[i] = translatedValue!;
1054+
}
1055+
1056+
wbe.AddPartitionBy(translatedPartitions);
1057+
1058+
return wbe!;
1059+
}
1060+
1061+
//anything to put in this case?
1062+
case
1063+
{
1064+
1065+
} when method.DeclaringType == typeof(IOrderRoot)
1066+
|| method.DeclaringType == typeof(IOrderThen):
1067+
{
1068+
if (!(Visit(methodCallExpression.Object) is RelationalWindowBuilderExpression wbe))
1069+
return QueryCompilationContext.NotTranslatedExpression;
1070+
1071+
if (TranslationFailed(arguments[0], Visit(RemoveObjectConvert(arguments[0])), out var sqlOject))
1072+
{
1073+
return QueryCompilationContext.NotTranslatedExpression;
1074+
}
1075+
1076+
wbe.AddOrdering(sqlOject!, method.Name == nameof(IOrderRoot.OrderBy)
1077+
|| method.Name == nameof(IOrderThen.ThenBy));
1078+
1079+
return wbe!;
1080+
}
1081+
1082+
//another open case
1083+
case
1084+
{
1085+
1086+
} when method.DeclaringType == typeof(IFrame):
1087+
{
1088+
if (!(Visit(methodCallExpression.Object) is RelationalWindowBuilderExpression wbe))
1089+
return QueryCompilationContext.NotTranslatedExpression;
1090+
1091+
var preceding = Visit(arguments[0]) as SqlConstantExpression;
1092+
1093+
if (preceding == null)
1094+
return QueryCompilationContext.NotTranslatedExpression;
1095+
1096+
var following = arguments.Count == 2 ? Visit(arguments[1]) as SqlConstantExpression : null;
1097+
1098+
if (following == null && arguments.Count == 2)
1099+
return QueryCompilationContext.NotTranslatedExpression;
1100+
1101+
//todo - should I key off the the string rows here? What about when someone has to override to add Groups?
1102+
wbe.AddFrame(method, preceding, following);
1103+
1104+
return wbe;
1105+
}
1106+
9921107
default:
9931108
{
9941109
scalarArguments = [];
995-
if (!TryTranslateAsEnumerableExpression(methodCallExpression.Object, out enumerableExpression)
1110+
if (!TryTranslateAsEnumerableExpression(methodCallExpression!.Object, out enumerableExpression)
9961111
&& TranslationFailed(methodCallExpression.Object, Visit(methodCallExpression.Object), out sqlObject))
9971112
{
9981113
return TranslateAsSubquery(methodCallExpression);

0 commit comments

Comments
 (0)