Add support for SQL window functions in ExpressiveSharp.EntityFrameworkCore#10
Add support for SQL window functions in ExpressiveSharp.EntityFrameworkCore#10
Conversation
…pressiveSharp.EntityFrameworkCore
There was a problem hiding this comment.
Pull request overview
Adds a new relational extension package to the ExpressiveSharp EF Core integration, enabling SQL window functions and indexed Select support via an opt-in plugin model (UseExpressives(o => o.UseRelationalExtensions())).
Changes:
- Introduces
ExpressiveSharp.EntityFrameworkCore.Relationalwith window function stubs (WindowFunction,Window) and EF Core translation/rendering infrastructure. - Adds a plugin system to
ExpressiveSharp.EntityFrameworkCore(IExpressivePlugin+ExpressiveOptionsBuilder) so relational features can be enabled selectively. - Extends the interceptor generator and stubs to support
Select((elem, index) => ...), plus a transformer that rewrites indexed selects toROW_NUMBER().
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/ExpressiveSharp.EntityFrameworkCore.Relational.Tests/WindowFunctionTests.cs | New SQL-shape + integration tests for window functions and indexed Select. |
| tests/ExpressiveSharp.EntityFrameworkCore.Relational.Tests/Models/WindowTestDbContext.cs | Minimal EF Core model/context for relational window-function tests. |
| tests/ExpressiveSharp.EntityFrameworkCore.Relational.Tests/Models/TestModels.cs | Test entities (Customer, Order). |
| tests/ExpressiveSharp.EntityFrameworkCore.Relational.Tests/ExpressiveSharp.EntityFrameworkCore.Relational.Tests.csproj | New MSTest project wiring for relational tests. |
| src/ExpressiveSharp/Extensions/RewritableQueryableLinqExtensions.cs | Adds delegate-stub overload for indexed Select on IRewritableQueryable<T>. |
| src/ExpressiveSharp.Generator/PolyfillInterceptorGenerator.cs | Adds interceptor emission for indexed Select(Func<T,int,TResult>). |
| src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/Internal/ExpressiveOptionsExtension.cs | Adds plugin application and plugin-based EF internal service provider hashing. |
| src/ExpressiveSharp.EntityFrameworkCore/IExpressivePlugin.cs | New plugin interface for registering services + transformers. |
| src/ExpressiveSharp.EntityFrameworkCore/Extensions/DbContextOptionsExtensions.cs | Adds UseExpressives(Action<ExpressiveOptionsBuilder>) overload for plugin configuration. |
| src/ExpressiveSharp.EntityFrameworkCore/ExpressiveOptionsBuilder.cs | New builder used by UseExpressives(...) to register plugins. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/WindowFunctions/WindowFunction.cs | Public window function stubs (translated to SQL). |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/WindowFunctions/WindowDefinition.cs | Marker/fluent API for building window specs in expression trees. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/WindowFunctions/Window.cs | Static entry point for window spec creation. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Transformers/RewriteIndexedSelectToRowNumber.cs | Rewrites indexed Queryable.Select to ROW_NUMBER() - 1. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/RelationalExpressivePlugin.cs | Registers relational translation services + transformers via plugin. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowSpecSqlExpression.cs | Intermediate SQL node carrying PARTITION/ORDER clauses. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowSpecMethodCallTranslator.cs | Translates Window/WindowDefinition calls into WindowSpecSqlExpression. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowFunctionTranslatorPlugin.cs | Registers method-call translators into EF Core pipeline. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowFunctionSqlNullabilityProcessor.cs | Marks window function SQL nodes as non-nullable during nullability processing. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowFunctionSqlExpression.cs | Custom SQL expression type for RANK/DENSE_RANK/NTILE window functions. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowFunctionParameterBasedSqlProcessorFactory.cs | Factory wiring for custom parameter-based SQL processor. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowFunctionParameterBasedSqlProcessor.cs | Hooks custom nullability processor into EF’s relational SQL processing. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowFunctionMethodCallTranslator.cs | Translates WindowFunction.* calls into EF SQL expressions. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/WindowFunctionEvaluatableExpressionFilter.cs | Prevents client-evaluation of window-function marker methods. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/ExpressiveRelationalQuerySqlGeneratorFactory.cs | Attempts to provide a SQL generator that can render custom window-function SQL nodes. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/Infrastructure/Internal/ExpressiveRelationalQuerySqlGenerator.cs | Adds SQL rendering for WindowFunctionSqlExpression. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/ExpressiveSharp.EntityFrameworkCore.Relational.csproj | New relational package project with EF Core Relational references per TFM. |
| src/ExpressiveSharp.EntityFrameworkCore.Relational/ExpressiveOptionsBuilderExtensions.cs | Adds UseRelationalExtensions() plugin registration extension. |
| README.md | Documents relational package installation and window function usage. |
| ExpressiveSharp.slnx | Adds the new relational project and its test project to the solution. |
| Directory.Packages.props | Adds central version for Microsoft.EntityFrameworkCore.Relational. |
| CLAUDE.md | Updates repo/module map and test listing to include relational project/tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/ExpressiveSharp.EntityFrameworkCore.Relational.Tests/WindowFunctionTests.cs
Outdated
Show resolved
Hide resolved
| Assert.AreEqual(10, results.Count); | ||
| // NTILE(4) with 10 rows: buckets of 3,3,2,2 | ||
| Assert.IsTrue(results.All(r => r.Quartile >= 1 && r.Quartile <= 4), | ||
| "All quartile values should be between 1 and 4"); | ||
| // Count per bucket | ||
| var bucketCounts = results.GroupBy(r => r.Quartile).OrderBy(g => g.Key).Select(g => g.Count()).ToList(); | ||
| Assert.AreEqual(4, bucketCounts.Count, "Should have exactly 4 buckets"); | ||
| } |
There was a problem hiding this comment.
Ntile_DistributesIntoBuckets computes bucket counts but doesn't assert the expected distribution (the comment says 3,3,2,2 for 10 rows into 4 buckets). As written, the test would still pass even if NTILE were translated incorrectly as long as values stay within 1..4 and all buckets appear. Add assertions for the expected per-bucket counts (and/or expected ordering) to make this test effective.
...eworkCore.RelationalExtensions/Infrastructure/Internal/WindowFunctionMethodCallTranslator.cs
Show resolved
Hide resolved
...eworkCore.Relational/Infrastructure/Internal/ExpressiveRelationalQuerySqlGeneratorFactory.cs
Outdated
Show resolved
Hide resolved
| /// Plugin that registers window function services into the EF Core service provider. | ||
| /// Auto-discovered via <see cref="ExpressivePluginAttribute"/> when the assembly is loaded. | ||
| /// </summary> | ||
| /// <summary> | ||
| /// Plugin that registers window function services into the EF Core service provider. | ||
| /// Activated via <c>.UseExpressives(o => o.UseRelationalExtensions())</c>. | ||
| /// </summary> |
There was a problem hiding this comment.
The XML docs on RelationalExpressivePlugin have two consecutive <summary> blocks, and the first one mentions auto-discovery via ExpressivePluginAttribute even though no such attribute is present/used in this PR. This will generate malformed/unhelpful docs and is misleading for consumers. Collapse to a single <summary> and describe the actual activation mechanism (UseExpressives(o => o.UseRelationalExtensions())).
| /// Plugin that registers window function services into the EF Core service provider. | |
| /// Auto-discovered via <see cref="ExpressivePluginAttribute"/> when the assembly is loaded. | |
| /// </summary> | |
| /// <summary> | |
| /// Plugin that registers window function services into the EF Core service provider. | |
| /// Activated via <c>.UseExpressives(o => o.UseRelationalExtensions())</c>. | |
| /// </summary> | |
| /// Plugin that registers window function services into the EF Core service provider, | |
| /// activated via <c>.UseExpressives(o => o.UseRelationalExtensions())</c>. | |
| /// </summary> |
src/ExpressiveSharp.EntityFrameworkCore/Infrastructure/Internal/ExpressiveOptionsExtension.cs
Show resolved
Hide resolved
...ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.Tests/Models/WindowTestDbContext.cs
Show resolved
Hide resolved
tests/ExpressiveSharp.EntityFrameworkCore.Relational.Tests/WindowFunctionTests.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'ExpressiveSharp Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.
| Benchmark suite | Current: 8bb95ff | Previous: ecfbc5e | Ratio |
|---|---|---|---|
ExpressiveSharp.Benchmarks.ExpressionReplacerBenchmarks.Replace_Method |
2741.0256703694663 ns (± 1505.0785066694834) |
2228.660212198893 ns (± 1112.9733320962775) |
1.23 |
ExpressiveSharp.Benchmarks.EFCoreQueryOverheadBenchmarks.WithExpressives_Method |
28230.931579589844 ns (± 9372.08233244819) |
22196.46024576823 ns (± 6676.912910179035) |
1.27 |
ExpressiveSharp.Benchmarks.ExpressionReplacerBenchmarks.Replace_BlockBody |
6257.154693603516 ns (± 2725.442629531928) |
3140.7159576416016 ns (± 64.07807556687932) |
1.99 |
ExpressiveSharp.Benchmarks.GeneratorBenchmarks.RunGenerator_NoiseChange(ExpressiveCount: 1) |
2730364.2083333335 ns (± 692274.6929494727) |
2136743.9348958335 ns (± 82866.55228766914) |
1.28 |
ExpressiveSharp.Benchmarks.GeneratorBenchmarks.RunGenerator_Incremental_ExpressiveChange(ExpressiveCount: 100) |
5457064.161458333 ns (± 710937.7021331617) |
4202663.296875 ns (± 310669.68796058506) |
1.30 |
ExpressiveSharp.Benchmarks.GeneratorBenchmarks.RunGenerator_Incremental_ExpressiveChange(ExpressiveCount: 1000) |
13321867.385416666 ns (± 488251.425311472) |
10671169.744791666 ns (± 358374.5233719214) |
1.25 |
This comment was automatically generated by workflow using github-action-benchmark.
…nhance WindowFunctionSqlExpression documentation
- Add arguments.Count guards to Rank/DenseRank/Ntile translator arms - Fix stale comment about seeded test data prices - Strengthen Ntile test with per-bucket count assertions - Remove unused using directive Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… utilize inner processors for nullability handling
…ePlugin for proper equality comparison
Summary
Adds SQL window function support (ROW_NUMBER, RANK, DENSE_RANK, NTILE) and indexed
Selectto ExpressiveSharp via a newExpressiveSharp.EntityFrameworkCore.Relationalpackage.Setup
What this enables
Ranking rows within a result set
Ranking with ties
Dense ranking (no gaps after ties)
Distributing rows into buckets
Indexed Select (zero-based row index)
Combining with ExpressiveSharp features
Architecture
ExpressiveSharp.EntityFrameworkCore.Relational— depends onMicrosoft.EntityFrameworkCore.Relational, keeping the base EF Core package provider-agnosticExpressiveOptionsBuilderwithUseRelationalExtensions()— explicit opt-in, no fragile assembly scanningRowNumberExpression; RANK/DENSE_RANK/NTILE use a self-renderingWindowFunctionSqlExpressionthat works with any provider'sQuerySqlGenerator(no generator replacement)IExpressionTreeTransformerthat rewritesQueryable.Select((item, index) => ...)to injectROW_NUMBER() - 1Test plan
🤖 Generated with Claude Code