Skip to content

Commit 08e9530

Browse files
committed
Added generic window function
1 parent f7d03fa commit 08e9530

File tree

16 files changed

+952
-86
lines changed

16 files changed

+952
-86
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
1414
<RootNamespace>Thinktecture</RootNamespace>
1515
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
16-
<LangVersion>10.0</LangVersion>
16+
<LangVersion>11.0</LangVersion>
1717
<Nullable>enable</Nullable>
1818
<NoWarn>$(NoWarn);CA1303;MSB3884;</NoWarn>
1919
<ImplicitUsings>enable</ImplicitUsings>

src/Thinktecture.EntityFrameworkCore.Relational/EntityFrameworkCore/Query/ExpressionTranslators/RelationalDbFunctionsTranslator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ internal RelationalDbFunctionsTranslator(ISqlExpressionFactory sqlExpressionFact
3434

3535
switch (method.Name)
3636
{
37+
case nameof(RelationalDbFunctionsExtensions.PartitionBy):
38+
{
39+
return new WindowFunctionPartitionByExpression(arguments.Skip(1).ToList());
40+
}
3741
case nameof(RelationalDbFunctionsExtensions.OrderBy):
3842
{
3943
var orderBy = arguments.Skip(1).Select(e => new OrderingExpression(_sqlExpressionFactory.ApplyDefaultTypeMapping(e), true)).ToList();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Linq.Expressions;
2+
using Microsoft.EntityFrameworkCore.Query;
3+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
4+
using Microsoft.EntityFrameworkCore.Storage;
5+
6+
namespace Thinktecture.EntityFrameworkCore.Query.SqlExpressions;
7+
8+
/// <summary>
9+
/// Represents PARTITION BY.
10+
/// </summary>
11+
public class WindowFunctionPartitionByExpression : SqlExpression, INotNullableSqlExpression
12+
{
13+
/// <summary>
14+
/// Partition by expressions.
15+
/// </summary>
16+
public IReadOnlyList<SqlExpression> PartitionBy { get; }
17+
18+
/// <inheritdoc />
19+
public WindowFunctionPartitionByExpression(IReadOnlyList<SqlExpression> partitionBy)
20+
: base(typeof(WindowFunctionPartitionByClause), RelationalTypeMapping.NullMapping)
21+
{
22+
PartitionBy = partitionBy ?? throw new ArgumentNullException(nameof(partitionBy));
23+
}
24+
25+
/// <inheritdoc />
26+
protected override Expression Accept(ExpressionVisitor visitor)
27+
{
28+
if (visitor is QuerySqlGenerator)
29+
throw new NotSupportedException("The window function contains some expressions not supported by the Entity Framework.");
30+
31+
return base.Accept(visitor);
32+
}
33+
34+
/// <inheritdoc />
35+
protected override Expression VisitChildren(ExpressionVisitor visitor)
36+
{
37+
var visited = visitor.VisitExpressions(PartitionBy);
38+
39+
return ReferenceEquals(visited, PartitionBy) ? this : new WindowFunctionPartitionByExpression(visited);
40+
}
41+
42+
/// <inheritdoc />
43+
protected override void Print(ExpressionPrinter expressionPrinter)
44+
{
45+
ArgumentNullException.ThrowIfNull(expressionPrinter);
46+
47+
expressionPrinter.VisitCollection(PartitionBy);
48+
}
49+
50+
/// <inheritdoc />
51+
public override bool Equals(object? obj)
52+
{
53+
return obj != null && (ReferenceEquals(this, obj) || Equals(obj as WindowFunctionPartitionByExpression));
54+
}
55+
56+
private bool Equals(WindowFunctionPartitionByExpression? expression)
57+
{
58+
return base.Equals(expression) && PartitionBy.SequenceEqual(expression.PartitionBy);
59+
}
60+
61+
/// <inheritdoc />
62+
public override int GetHashCode()
63+
{
64+
var hash = new HashCode();
65+
hash.Add(base.GetHashCode());
66+
67+
for (var i = 0; i < PartitionBy.Count; i++)
68+
{
69+
hash.Add(PartitionBy[i]);
70+
}
71+
72+
return hash.ToHashCode();
73+
}
74+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Thinktecture.EntityFrameworkCore;
2+
3+
/// <summary>
4+
/// Helper class to attach extension methods to.
5+
/// </summary>
6+
public sealed class WindowFunctionPartitionByClause
7+
{
8+
private WindowFunctionPartitionByClause()
9+
{
10+
}
11+
}

src/Thinktecture.EntityFrameworkCore.Relational/Extensions/RelationalDbFunctionsExtensions.cs

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public static long RowNumber<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12,
216216
}
217217

218218
/// <summary>
219-
/// Definition of the ORDER BY clause of a the ROW_NUMBER expression.
219+
/// Definition of the ORDER BY clause of a the window function.
220220
/// </summary>
221221
/// <remarks>
222222
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
@@ -228,7 +228,7 @@ public static WindowFunctionOrderByClause OrderBy<T>(this DbFunctions _, T colum
228228
}
229229

230230
/// <summary>
231-
/// Definition of the ORDER BY clause of a the ROW_NUMBER expression.
231+
/// Definition of the ORDER BY clause of a the window function.
232232
/// </summary>
233233
/// <remarks>
234234
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
@@ -240,7 +240,7 @@ public static WindowFunctionOrderByClause OrderByDescending<T>(this DbFunctions
240240
}
241241

242242
/// <summary>
243-
/// Definition of the ORDER BY clause of a the ROW_NUMBER expression.
243+
/// Definition of the ORDER BY clause of a the window function.
244244
/// </summary>
245245
/// <remarks>
246246
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
@@ -252,7 +252,7 @@ public static WindowFunctionOrderByClause ThenBy<T>(this WindowFunctionOrderByCl
252252
}
253253

254254
/// <summary>
255-
/// Definition of the ORDER BY clause of a the ROW_NUMBER expression.
255+
/// Definition of the ORDER BY clause of a the window function.
256256
/// </summary>
257257
/// <remarks>
258258
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
@@ -263,5 +263,65 @@ public static WindowFunctionOrderByClause ThenByDescending<T>(this WindowFunctio
263263
throw new InvalidOperationException("This method is for use with Entity Framework Core only and has no in-memory implementation.");
264264
}
265265

266+
/// <summary>
267+
/// Definition of the PARTITION BY clause of a window function.
268+
/// </summary>
269+
/// <remarks>
270+
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
271+
/// </remarks>
272+
/// <exception cref="InvalidOperationException">Is thrown if executed in-memory.</exception>
273+
public static WindowFunctionPartitionByClause PartitionBy<T1>(this DbFunctions _, T1 column1)
274+
{
275+
throw new InvalidOperationException("This method is for use with Entity Framework Core only and has no in-memory implementation.");
276+
}
277+
278+
/// <summary>
279+
/// Definition of the PARTITION BY clause of a window function.
280+
/// </summary>
281+
/// <remarks>
282+
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
283+
/// </remarks>
284+
/// <exception cref="InvalidOperationException">Is thrown if executed in-memory.</exception>
285+
public static WindowFunctionPartitionByClause PartitionBy<T1, T2>(this DbFunctions _, T1 column1, T2 column2)
286+
{
287+
throw new InvalidOperationException("This method is for use with Entity Framework Core only and has no in-memory implementation.");
288+
}
289+
290+
/// <summary>
291+
/// Definition of the PARTITION BY clause of a window function.
292+
/// </summary>
293+
/// <remarks>
294+
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
295+
/// </remarks>
296+
/// <exception cref="InvalidOperationException">Is thrown if executed in-memory.</exception>
297+
public static WindowFunctionPartitionByClause PartitionBy<T1, T2, T3>(this DbFunctions _, T1 column1, T2 column2, T3 column3)
298+
{
299+
throw new InvalidOperationException("This method is for use with Entity Framework Core only and has no in-memory implementation.");
300+
}
301+
302+
/// <summary>
303+
/// Definition of the PARTITION BY clause of a window function.
304+
/// </summary>
305+
/// <remarks>
306+
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
307+
/// </remarks>
308+
/// <exception cref="InvalidOperationException">Is thrown if executed in-memory.</exception>
309+
public static WindowFunctionPartitionByClause PartitionBy<T1, T2, T3, T4>(this DbFunctions _, T1 column1, T2 column2, T3 column3, T4 column4)
310+
{
311+
throw new InvalidOperationException("This method is for use with Entity Framework Core only and has no in-memory implementation.");
312+
}
313+
314+
/// <summary>
315+
/// Definition of the PARTITION BY clause of a window function.
316+
/// </summary>
317+
/// <remarks>
318+
/// This method is for use with Entity Framework Core only and has no in-memory implementation.
319+
/// </remarks>
320+
/// <exception cref="InvalidOperationException">Is thrown if executed in-memory.</exception>
321+
public static WindowFunctionPartitionByClause PartitionBy<T1, T2, T3, T4, T5>(this DbFunctions _, T1 column1, T2 column2, T3 column3, T4 column4, T5 column5)
322+
{
323+
throw new InvalidOperationException("This method is for use with Entity Framework Core only and has no in-memory implementation.");
324+
}
325+
266326
// ReSharper restore UnusedParameter.Global
267327
}

src/Thinktecture.EntityFrameworkCore.Relational/Extensions/RelationalExpressionVisitorExtensions.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Linq.Expressions;
2+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
23

34
// ReSharper disable once CheckNamespace
45
namespace Thinktecture;
@@ -20,19 +21,27 @@ public static class RelationalExpressionVisitorExtensions
2021
public static IReadOnlyList<T> VisitExpressions<T>(this ExpressionVisitor visitor, IReadOnlyList<T> expressions)
2122
where T : Expression
2223
{
23-
ArgumentNullException.ThrowIfNull(visitor);
24-
ArgumentNullException.ThrowIfNull(expressions);
24+
T[]? visitedExpression = null;
2525

26-
var visitedExpressions = new List<T>();
27-
var hasChanges = false;
28-
29-
foreach (var expression in expressions)
26+
for (var i = 0; i < expressions.Count; i++)
3027
{
31-
var visitedExpression = (T)visitor.Visit(expression);
32-
visitedExpressions.Add(visitedExpression);
33-
hasChanges |= !ReferenceEquals(visitedExpression, expression);
28+
var expression = expressions[i];
29+
var visited = (T)visitor.Visit(expression);
30+
31+
if (visited != expression && visitedExpression is null)
32+
{
33+
visitedExpression = new T[expressions.Count];
34+
35+
for (var j = 0; j < i; j++)
36+
{
37+
visitedExpression[j] = expressions[j];
38+
}
39+
}
40+
41+
if (visitedExpression is not null)
42+
visitedExpression[i] = visited;
3443
}
3544

36-
return hasChanges ? visitedExpressions.AsReadOnly() : expressions;
45+
return visitedExpression ?? expressions;
3746
}
3847
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace Thinktecture;
4+
5+
/// <summary>
6+
/// Represents a window function name.
7+
/// </summary>
8+
public abstract partial class WindowFunction
9+
{
10+
private const string _REGEX_PATTERN = "^[A-Z_]+$";
11+
private const RegexOptions _REGEX_OPTIONS = RegexOptions.IgnoreCase | RegexOptions.Compiled;
12+
13+
#if NET7_0_OR_GREATER
14+
[GeneratedRegex(_REGEX_PATTERN, _REGEX_OPTIONS)]
15+
private static partial Regex ValidateName();
16+
#else
17+
private static readonly Regex _validateName = new Regex(_REGEX_PATTERN, _REGEX_OPTIONS);
18+
#endif
19+
20+
/// <summary>
21+
/// Name of the function.
22+
/// </summary>
23+
public string Name { get; }
24+
25+
/// <summary>
26+
/// Return type of the function.
27+
/// </summary>
28+
public Type ReturnType { get; }
29+
30+
/// <summary>
31+
/// Indication whether to use '*' when no arguments are provided.
32+
/// </summary>
33+
public bool UseStarWhenNoArguments { get; }
34+
35+
/// <summary>
36+
/// Initializes a new instance of <see cref="WindowFunction"/>.
37+
/// </summary>
38+
/// <param name="name">The name of the window function.</param>
39+
/// <param name="returnType">Return type of the function.</param>
40+
/// <param name="useStarWhenNoArguments">Indication whether to use '*' when no arguments are provided.</param>
41+
protected WindowFunction(string name, Type returnType, bool useStarWhenNoArguments)
42+
{
43+
Name = EnsureValidName(name);
44+
ReturnType = returnType ?? throw new ArgumentNullException(nameof(returnType));
45+
UseStarWhenNoArguments = useStarWhenNoArguments;
46+
}
47+
48+
private static string EnsureValidName(string name)
49+
{
50+
if (String.IsNullOrWhiteSpace(name))
51+
throw new ArgumentException("Name must not be empty.");
52+
53+
name = name.Trim();
54+
55+
#if NET7_0_OR_GREATER
56+
if (!ValidateName().IsMatch(name))
57+
#else
58+
if (!_validateName.IsMatch(name))
59+
#endif
60+
throw new ArgumentException("The name must consist of characters A-Z and may contain an underscore.");
61+
62+
return name;
63+
}
64+
65+
/// <summary>
66+
/// Deconstruction of the window function.
67+
/// </summary>
68+
/// <param name="name">The name of the function.</param>
69+
/// <param name="returnType">The return type of the function.</param>
70+
/// <param name="useStarWhenNoArguments">Indication whether to use '*' when no arguments are provided.</param>
71+
public void Deconstruct(out string name, out Type returnType, out bool useStarWhenNoArguments)
72+
{
73+
name = Name;
74+
returnType = ReturnType;
75+
useStarWhenNoArguments = UseStarWhenNoArguments;
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Represents a window function name.
81+
/// </summary>
82+
public sealed class WindowFunction<TResult> : WindowFunction, IEquatable<WindowFunction<TResult>>
83+
{
84+
/// <summary>
85+
/// Initializes a new instance of <see cref="WindowFunction{TResult}"/>.
86+
/// </summary>
87+
/// <param name="name">The name of the window function</param>
88+
/// <param name="useStarWhenNoArguments">Indication whether to use '*' when no arguments are provided.</param>
89+
public WindowFunction(
90+
string name,
91+
bool useStarWhenNoArguments = false)
92+
: base(name, typeof(TResult), useStarWhenNoArguments)
93+
{
94+
}
95+
96+
/// <inheritdoc />
97+
public override bool Equals(object? obj)
98+
{
99+
return Equals(obj as WindowFunction<TResult>);
100+
}
101+
102+
/// <inheritdoc />
103+
public bool Equals(WindowFunction<TResult>? other)
104+
{
105+
if (ReferenceEquals(null, other))
106+
return false;
107+
if (ReferenceEquals(this, other))
108+
return true;
109+
110+
return Name == other.Name;
111+
}
112+
113+
/// <inheritdoc />
114+
public override int GetHashCode()
115+
{
116+
return Name.GetHashCode();
117+
}
118+
119+
/// <inheritdoc />
120+
public override string ToString()
121+
{
122+
return Name;
123+
}
124+
}

src/Thinktecture.EntityFrameworkCore.SqlServer/EntityFrameworkCore/Infrastructure/SqlServerDbContextOptionsExtension.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ public void ApplyServices(IServiceCollection services)
179179

180180
if (_relationalOptions.AddSchemaRespectingComponents)
181181
services.AddSingleton<IMigrationOperationSchemaSetter, SqlServerMigrationOperationSchemaSetter>();
182+
183+
if (AddWindowFunctionsSupport)
184+
{
185+
var lifetime = GetLifetime<IEvaluatableExpressionFilterPlugin>();
186+
services.Add(ServiceDescriptor.Describe(typeof(IEvaluatableExpressionFilterPlugin), typeof(WindowFunctionEvaluatableExpressionFilterPlugin), lifetime));
187+
188+
}
182189
}
183190

184191
/// <summary>

0 commit comments

Comments
 (0)