Skip to content

Commit 1becb60

Browse files
authored
Merge pull request #56 from EFNext/feat/relational-extensions-stable
Stabilize RelationalExtensions
2 parents e74afaf + 7846f12 commit 1becb60

18 files changed

Lines changed: 722 additions & 108 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Abstractions (net8.0;ne
101101
LAST_VALUE, NTH_VALUE), Window, OrderedWindowDefinition,
102102
PartitionedWindowDefinition, FramedWindowDefinition, WindowFrameBound
103103
104-
ExpressiveSharp.EntityFrameworkCore.RelationalExtensions (net8.0;net10.0, experimental)
104+
ExpressiveSharp.EntityFrameworkCore.RelationalExtensions (net8.0;net10.0)
105105
├── ExpressiveSharp.EntityFrameworkCore
106106
├── ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Abstractions
107107
├── EF Core Relational 8.0.25 / 10.0.0
@@ -119,7 +119,7 @@ ExpressiveSharp.EntityFrameworkCore.CodeFixers (Roslyn analyzer, netstandard2.0)
119119

120120
### Diagnostics
121121

122-
20 diagnostic codes (EXP0001–EXP0012, EXP0018 in `src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs`, EXP0013 in CodeFixers, EXP0014–EXP0020 for `[ExpressiveFor]` validation). Key ones: EXP0001 (requires body), EXP0004 (block body requires opt-in), EXP0008 (unsupported operation, default value used), EXP0018 (unsupported operation ignored, e.g. alignment specifiers), EXP0019 (`[ExpressiveFor]` conflicts with `[Expressive]`).
122+
22 diagnostic codes (EXP0001–EXP0012, EXP0018 in `src/ExpressiveSharp.Generator/Infrastructure/Diagnostics.cs`, EXP0013 in CodeFixers, EXP0014–EXP0020 for `[ExpressiveFor]` validation, EXP0036/EXP0037 in `WindowFunctionLiteralArgsAnalyzer`). Key ones: EXP0001 (requires body), EXP0004 (block body requires opt-in), EXP0008 (unsupported operation, default value used), EXP0018 (unsupported operation ignored, e.g. alignment specifiers), EXP0019 (`[ExpressiveFor]` conflicts with `[Expressive]`), EXP0036 (`Ntile` non-positive literal), EXP0037 (`Lag`/`Lead` negative literal offset).
123123

124124
## Testing
125125

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Mark computed properties and methods with `[Expressive]` to generate companion e
113113
| External member mapping | `[ExpressiveFor]` for BCL/third-party members |
114114
| Tuples, index/range, `with`, collection expressions | And more modern C# syntax |
115115
| Expression transformers | Built-in + custom `IExpressionTreeTransformer` pipeline |
116-
| SQL window functions | ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST, SUM/AVG/COUNT/MIN/MAX OVER, LAG/LEAD, FIRST_VALUE/LAST_VALUE/NTH_VALUE with ROWS/RANGE frames (experimental) |
116+
| SQL window functions | ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST, SUM/AVG/COUNT/MIN/MAX OVER, LAG/LEAD, FIRST_VALUE/LAST_VALUE/NTH_VALUE with ROWS/RANGE frames |
117117
| Hot reload | Compatible with `dotnet watch` — edits to `[Expressive]` bodies propagate to generated expression trees |
118118

119119
See the [full documentation](https://efnext.github.io/ExpressiveSharp/guide/introduction) for detailed usage, [reference](https://efnext.github.io/ExpressiveSharp/reference/expressive-attribute), and [recipes](https://efnext.github.io/ExpressiveSharp/recipes/computed-properties).

docs/advanced/limitations.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -158,22 +158,18 @@ String interpolation with format specifiers like `$"{Price:F2}"` introduces a `T
158158
| `while`/`do-while`, `try`/`catch`, `async`/`await` | Not supported |
159159
| Assignments, `++`, `--` | Not supported |
160160

161-
## Window Functions: Experimental Status
161+
## Window Functions: Provider Support
162162

163-
The `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` package providing window functions (ROW_NUMBER, RANK, DENSE_RANK, NTILE) is **experimental**.
164-
165-
::: warning
166-
EF Core has an [open issue](https://github.com/dotnet/efcore/issues/12747) for native window function support. This package may be superseded when that ships. The API surface may change in future releases.
167-
:::
168-
169-
Window functions are limited to relational providers compatible with SQL:2003 window function syntax:
163+
The `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` package implements SQL window functions for relational providers compatible with SQL:2003 window function syntax:
170164

171165
| Provider | Status |
172166
|---|---|
173-
| SQL Server | Supported |
167+
| SQL Server | Supported (`NTH_VALUE` not implemented by SQL Server itself) |
174168
| PostgreSQL | Supported |
175169
| SQLite | Supported |
176170
| MySQL | Supported |
177171
| Oracle | Supported |
178172

179173
Non-relational providers (Cosmos DB, in-memory) are not supported for window functions.
174+
175+
EF Core also tracks native window function support in [dotnet/efcore#12747](https://github.com/dotnet/efcore/issues/12747); see the [window functions guide](../guide/window-functions#forward-compatibility) for forward-compatibility notes.

docs/guide/quickstart.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ dotnet add package ExpressiveSharp.MongoDB
3939
| `ExpressiveSharp.Abstractions` | Lightweight — `[Expressive]` attribute, `[ExpressiveFor]`, `IExpressionTreeTransformer`, source generator only (no runtime services) |
4040
| `ExpressiveSharp.EntityFrameworkCore` | EF Core integration — `UseExpressives()`, `ExpressiveDbSet<T>`, Include/ThenInclude, async methods, analyzers and code fixes |
4141
| `ExpressiveSharp.MongoDB` | MongoDB integration — `.AsExpressive()` on `IMongoCollection<T>`, MQL aggregation translation |
42-
| `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` | SQL window functions — ranking (ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST), aggregate (SUM, AVG, COUNT, MIN, MAX), and navigation (LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE). Experimental. |
42+
| `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` | SQL window functions — ranking (ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST), aggregate (SUM, AVG, COUNT, MIN, MAX), and navigation (LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE) with PARTITION BY / ORDER BY / ROWS\|RANGE frame support, plus indexed Select. |
4343

4444
## Step 2 — Define Your Entities
4545

docs/guide/window-functions.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
ExpressiveSharp provides SQL window function support through the `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` package. This enables ranking (ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST), aggregate (SUM, AVG, COUNT, MIN, MAX), and navigation (LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE) functions directly in LINQ queries with a fluent window specification API.
44

5-
::: warning Experimental
6-
This package is experimental. EF Core has an [open issue (#12747)](https://github.com/dotnet/efcore/issues/12747) for native window function support. This package may be superseded when that ships.
7-
:::
8-
95
## Installation
106

117
```bash
@@ -239,14 +235,19 @@ Window functions are supported across all major relational database providers:
239235

240236
| Provider | Supported | Notes |
241237
|----------|-----------|-------|
242-
| SQLite | Yes | |
243-
| SQL Server | Yes | `NTH_VALUE` is not supported |
244-
| PostgreSQL | Yes | |
245-
| MySQL | Yes | |
238+
| SQLite | Yes | Full support (3.25+) |
239+
| PostgreSQL | Yes | Full support |
240+
| SQL Server | Yes | See note on `NTH_VALUE` below |
241+
| MySQL (Pomelo) | Yes | Pomelo provider unavailable on .NET 10 until upstream support returns |
246242
| Oracle | Yes | |
243+
| Cosmos | No | Cosmos has no SQL window functions |
247244

248245
The generated SQL uses standard ANSI window function syntax. Each provider translates the expressions using its native SQL dialect.
249246

247+
::: warning `NTH_VALUE` on SQL Server
248+
SQL Server does not implement the `NTH_VALUE` window function. Queries using `WindowFunction.NthValue(...)` will fail at execution with a "not a recognized built-in function" error. Workaround: use `FIRST_VALUE`/`LAST_VALUE` with a constrained frame, or switch to PostgreSQL/SQLite/MySQL.
249+
:::
250+
250251
## Full Configuration Example
251252

252253
```csharp
@@ -284,6 +285,29 @@ var rankings = ctx.Orders
284285
Window functions are implemented as a plugin using the `IExpressivePlugin` architecture. The `UseRelationalExtensions()` call registers custom EF Core services and expression translators that handle the `WindowFunction.*` method calls during SQL generation.
285286
:::
286287

288+
## Argument validation
289+
290+
The translator rejects clearly-invalid arguments at translation time (before the query reaches the database) and throws `InvalidOperationException`:
291+
292+
- `WindowFunction.Ntile(n, ...)` with literal `n <= 0`
293+
- `WindowFunction.Lag(expr, n, ...)` / `WindowFunction.Lead(expr, n, ...)` with literal `n < 0`
294+
- `WindowFunction.NthValue(expr, n, ...)` with literal `n < 1`
295+
- `WindowFunction.PercentRank(...)` / `WindowFunction.CumeDist(...)` constructed without an `OrderBy` (only reachable via manually-built expression trees, since the fluent builder requires an ordered window)
296+
297+
Non-literal (parameter) values are forwarded to the database, which performs its own validation.
298+
299+
## Forward compatibility
300+
301+
EF Core tracks native window function support in [dotnet/efcore#12747](https://github.com/dotnet/efcore/issues/12747). When that ships, the fluent builder API in this package is expected to remain stable; the underlying translator may delegate to EF's primitives in a future release.
302+
303+
## Upgrading from earlier versions
304+
305+
The package was previously labeled experimental. Upgrading is API-compatible; three observable behaviors changed:
306+
307+
- Direct invocation of a `WindowFunction.*` stub (i.e. outside an EF Core query) now throws an exception that names the method and points at this guide.
308+
- `Ntile(0)` / `Ntile(-1)`, negative literal `Lag`/`Lead` offsets, and `NthValue(0)` now throw `InvalidOperationException` at translation time. Previously these reached the database and produced a provider-specific error.
309+
- New analyzer warnings **EXP0036** (`Ntile` non-positive literal buckets) and **EXP0037** (`Lag`/`Lead` negative literal offsets) may surface on existing code.
310+
287311
## Next Steps
288312

289313
- [EF Core Integration](./integrations/ef-core) -- full EF Core setup and features

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ features:
4747

4848
- icon: "\U0001F4CA"
4949
title: SQL Window Functions
50-
details: "Ranking (ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST), aggregate (SUM, AVG, COUNT, MIN, MAX), and navigation (LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE) functions with a fluent PARTITION BY / ORDER BY / frame API. Experimental — via the RelationalExtensions package."
50+
details: "Ranking (ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST), aggregate (SUM, AVG, COUNT, MIN, MAX), and navigation (LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE) functions with a fluent PARTITION BY / ORDER BY / frame API — via the RelationalExtensions package."
5151

5252
- icon: "\U0001F527"
5353
title: Customizable Transformer Pipeline
@@ -116,4 +116,4 @@ Computed properties are **inlined into the provider's native query language**
116116
| [`ExpressiveSharp.Abstractions`](https://www.nuget.org/packages/ExpressiveSharp.Abstractions/) | Lightweight — attributes (`[Expressive]`, `[ExpressiveFor]`), `IExpressionTreeTransformer`, source generator only |
117117
| [`ExpressiveSharp.EntityFrameworkCore`](https://www.nuget.org/packages/ExpressiveSharp.EntityFrameworkCore/) | EF Core integration — `UseExpressives()`, `ExpressiveDbSet<T>`, Include/ThenInclude, async methods |
118118
| [`ExpressiveSharp.MongoDB`](https://www.nuget.org/packages/ExpressiveSharp.MongoDB/) | MongoDB integration — `.AsExpressive()` on `IMongoCollection<T>`, MQL translation |
119-
| [`ExpressiveSharp.EntityFrameworkCore.RelationalExtensions`](https://www.nuget.org/packages/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/) | SQL window functions — ROW_NUMBER, RANK, DENSE_RANK, NTILE (experimental) |
119+
| [`ExpressiveSharp.EntityFrameworkCore.RelationalExtensions`](https://www.nuget.org/packages/ExpressiveSharp.EntityFrameworkCore.RelationalExtensions/) | SQL window functions — ranking, aggregate, navigation with ROWS/RANGE frames |

docs/recipes/window-functions-ranking.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
This recipe shows how to use SQL window functions in EF Core LINQ queries via the `ExpressiveSharp.EntityFrameworkCore.RelationalExtensions` package. Coverage includes ranking (ROW_NUMBER, RANK, DENSE_RANK, NTILE, PERCENT_RANK, CUME_DIST), aggregate (SUM, AVG, COUNT, MIN, MAX), and navigation (LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTH_VALUE) functions.
44

5-
::: warning Experimental
6-
This package is experimental. EF Core has an [open issue](https://github.com/dotnet/efcore/issues/12747) for native window function support -- this package may be superseded when that ships.
7-
:::
8-
95
## Setup
106

117
Install the package:
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace ExpressiveSharp.EntityFrameworkCore.CodeFixers;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class WindowFunctionLiteralArgsAnalyzer : DiagnosticAnalyzer
11+
{
12+
private const string WindowFunctionType =
13+
"ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.WindowFunctions.WindowFunction";
14+
15+
public static readonly DiagnosticDescriptor NtileRequiresPositiveBuckets = new(
16+
id: "EXP0036",
17+
title: "WindowFunction.Ntile requires a positive bucket count",
18+
messageFormat: "WindowFunction.Ntile requires a positive bucket count; literal value {0} produces invalid SQL",
19+
category: "Usage",
20+
defaultSeverity: DiagnosticSeverity.Warning,
21+
isEnabledByDefault: true,
22+
description: "NTILE(n) divides ordered rows into n buckets. SQL requires n >= 1; non-positive values raise a database error at execution time.");
23+
24+
public static readonly DiagnosticDescriptor NavigationOffsetMustBeNonNegative = new(
25+
id: "EXP0037",
26+
title: "WindowFunction.Lag/Lead offset must be non-negative",
27+
messageFormat: "WindowFunction.{0} offset must be non-negative; literal value {1} is rejected during EF translation",
28+
category: "Usage",
29+
defaultSeverity: DiagnosticSeverity.Warning,
30+
isEnabledByDefault: true,
31+
description: "LAG and LEAD offsets count rows backward or forward from the current row. SQL requires the offset to be >= 0; negative literals are rejected during EF translation.");
32+
33+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
34+
ImmutableArray.Create(NtileRequiresPositiveBuckets, NavigationOffsetMustBeNonNegative);
35+
36+
public override void Initialize(AnalysisContext context)
37+
{
38+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
39+
context.EnableConcurrentExecution();
40+
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
41+
}
42+
43+
private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
44+
{
45+
var invocation = (InvocationExpressionSyntax)context.Node;
46+
47+
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
48+
return;
49+
50+
var name = memberAccess.Name.Identifier.Text;
51+
if (name != "Ntile" && name != "Lag" && name != "Lead")
52+
return;
53+
54+
var symbolInfo = context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken);
55+
if (symbolInfo.Symbol is not IMethodSymbol method)
56+
return;
57+
58+
if (method.ContainingType?.ToDisplayString() != WindowFunctionType)
59+
return;
60+
61+
var args = invocation.ArgumentList.Arguments;
62+
if (args.Count == 0)
63+
return;
64+
65+
switch (name)
66+
{
67+
case "Ntile":
68+
if (TryGetIntLiteral(context.SemanticModel, args[0].Expression, context.CancellationToken, out var buckets) && buckets <= 0)
69+
{
70+
context.ReportDiagnostic(Diagnostic.Create(
71+
NtileRequiresPositiveBuckets,
72+
args[0].GetLocation(),
73+
buckets));
74+
}
75+
break;
76+
77+
case "Lag":
78+
case "Lead":
79+
if (args.Count >= 2 &&
80+
TryGetIntLiteral(context.SemanticModel, args[1].Expression, context.CancellationToken, out var offset) &&
81+
offset < 0)
82+
{
83+
context.ReportDiagnostic(Diagnostic.Create(
84+
NavigationOffsetMustBeNonNegative,
85+
args[1].GetLocation(),
86+
name,
87+
offset));
88+
}
89+
break;
90+
}
91+
}
92+
93+
private static bool TryGetIntLiteral(
94+
SemanticModel semanticModel,
95+
ExpressionSyntax expression,
96+
System.Threading.CancellationToken cancellationToken,
97+
out int value)
98+
{
99+
var constant = semanticModel.GetConstantValue(expression, cancellationToken);
100+
if (constant.HasValue && constant.Value is int i)
101+
{
102+
value = i;
103+
return true;
104+
}
105+
106+
value = 0;
107+
return false;
108+
}
109+
}

0 commit comments

Comments
 (0)