Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ public virtual IReadOnlyList<ScaffoldedFile> GeneratePrecompiledQueries(
_code
.AppendLine()
.AppendLine("#pragma warning disable EF9100 // Precompiled query is experimental")
.AppendLine("#pragma warning disable EF9001 // The query may use experimental APIs, e.g. window functions")
// Auto-generated code has the nullable context disabled, but the interceptor signatures contain nullable annotations
.AppendLine("#nullable enable annotations")
.AppendLine()
Expand Down
60 changes: 60 additions & 0 deletions src/EFCore.Relational/EFCore.Relational.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -8992,6 +8992,10 @@
},
{
"Member": "virtual System.Linq.Expressions.Expression VisitValues(Microsoft.EntityFrameworkCore.Query.SqlExpressions.ValuesExpression valuesExpression);"
},
{
"Member": "virtual System.Linq.Expressions.Expression VisitWindowFunction(Microsoft.EntityFrameworkCore.Query.SqlExpressions.WindowFunctionExpression windowFunctionExpression);",
"Stage": "Experimental"
}
],
"Properties": [
Expand Down Expand Up @@ -9949,6 +9953,10 @@
{
"Member": "RelationalCompiledQueryCacheKey(Microsoft.EntityFrameworkCore.Query.CompiledQueryCacheKeyGenerator.CompiledQueryCacheKey compiledQueryCacheKey, bool useRelationalNulls, Microsoft.EntityFrameworkCore.QuerySplittingBehavior? querySplittingBehavior, bool shouldBuffer);"
},
{
"Member": "RelationalCompiledQueryCacheKey(Microsoft.EntityFrameworkCore.Query.CompiledQueryCacheKeyGenerator.CompiledQueryCacheKey compiledQueryCacheKey, bool useRelationalNulls, Microsoft.EntityFrameworkCore.QuerySplittingBehavior? querySplittingBehavior, bool shouldBuffer, bool useWindowFunctionAggregates);",
"Stage": "Experimental"
},
{
"Member": "RelationalCompiledQueryCacheKey();"
},
Expand Down Expand Up @@ -10864,6 +10872,10 @@
{
"Member": "virtual TBuilder UseRelationalNulls(bool useRelationalNulls = true);"
},
{
"Member": "virtual TBuilder UseWindowFunctionAggregates(bool useWindowFunctionAggregates = true);",
"Stage": "Experimental"
},
{
"Member": "virtual TBuilder WithOption(System.Func<TExtension, TExtension> setAction);"
}
Expand Down Expand Up @@ -13549,6 +13561,10 @@
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Infrastructure.RelationalOptionsExtension WithUseRelationalNulls(bool useRelationalNulls);"
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Infrastructure.RelationalOptionsExtension WithUseWindowFunctionAggregates(bool useWindowFunctionAggregates);",
"Stage": "Experimental"
}
],
"Properties": [
Expand Down Expand Up @@ -13596,6 +13612,10 @@
},
{
"Member": "virtual bool UseRelationalNulls { get; }"
},
{
"Member": "virtual bool UseWindowFunctionAggregates { get; }",
"Stage": "Experimental"
}
]
},
Expand Down Expand Up @@ -14399,6 +14419,10 @@
{
"Member": "virtual bool IsValidSelectExpressionForExecuteUpdate(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression selectExpression, Microsoft.EntityFrameworkCore.Query.SqlExpressions.TableExpressionBase targetTable, out Microsoft.EntityFrameworkCore.Query.SqlExpressions.TableExpression? tableExpression);"
},
{
"Member": "virtual bool IsWindowFunctionSupported(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlFunctionExpression function);",
"Stage": "Experimental"
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Query.ShapedQueryExpression? TransformJsonQueryToTable(Microsoft.EntityFrameworkCore.Query.JsonQueryExpression jsonQueryExpression);"
},
Expand Down Expand Up @@ -14479,6 +14503,10 @@
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Query.SqlAliasManager SqlAliasManager { get; }"
},
{
"Member": "virtual bool UseWindowFunctionAggregates { get; }",
"Stage": "Experimental"
}
]
},
Expand Down Expand Up @@ -17945,6 +17973,10 @@
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression VisitSqlUnary(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlUnaryExpression sqlUnaryExpression, bool allowOptimizedExpansion, out bool nullable);"
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression VisitWindowFunction(Microsoft.EntityFrameworkCore.Query.SqlExpressions.WindowFunctionExpression windowFunctionExpression, bool allowOptimizedExpansion, out bool nullable);",
"Stage": "Experimental"
}
],
"Properties": [
Expand Down Expand Up @@ -19374,6 +19406,34 @@
"Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.ViewColumnBuilder<TProperty> HasColumnName(string? name);"
}
]
},
{
"Type": "class Microsoft.EntityFrameworkCore.Query.SqlExpressions.WindowFunctionExpression : Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression",
"Stage": "Experimental",
"Methods": [
{
"Member": "WindowFunctionExpression(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression function, System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression>? partitions, System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.OrderingExpression>? orderings, Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping? typeMapping);",
"Stage": "Experimental"
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Query.SqlExpressions.WindowFunctionExpression Update(Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression function, System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression> partitions, System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.OrderingExpression> orderings);",
"Stage": "Experimental"
}
],
"Properties": [
{
"Member": "virtual Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression Function { get; }",
"Stage": "Experimental"
},
{
"Member": "virtual System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.OrderingExpression> Orderings { get; }",
"Stage": "Experimental"
},
{
"Member": "virtual System.Collections.Generic.IReadOnlyList<Microsoft.EntityFrameworkCore.Query.SqlExpressions.SqlExpression> Partitions { get; }",
"Stage": "Experimental"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.EntityFrameworkCore.Infrastructure;

Expand Down Expand Up @@ -163,6 +164,28 @@ public virtual TBuilder ExecutionStrategy(
public virtual TBuilder UseParameterizedCollectionMode(ParameterTranslationMode parameterizedCollectionMode)
=> WithOption(e => (TExtension)e.WithUseParameterizedCollectionMode(parameterizedCollectionMode));

/// <summary>
/// Configures the context to translate aggregates which are projected alongside the elements of a grouping as SQL window
/// functions, e.g. <c>GroupBy(e =&gt; e.Key).Select(g =&gt; new { Elements = g.ToList(), Sum = g.Sum(e =&gt; e.Value) })</c>
/// becomes <c>SUM(Value) OVER (PARTITION BY Key)</c> over the ungrouped rows.
/// </summary>
/// <remarks>
/// <para>
/// Without this option such a projection groups on the server and then joins the source back to itself to get the elements
/// back, reading everything twice. Projections which this option cannot translate keep that existing translation.
/// </para>
/// <para>
/// This is an experimental API used to gather feedback on
/// <see href="https://github.com/dotnet/efcore/issues/12747">window function support</see>; it may change or be removed in
/// a future release.
/// </para>
/// </remarks>
/// <param name="useWindowFunctionAggregates"><see langword="true" /> to enable the translation; <see langword="false" /> to disable it.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
[Experimental(EFDiagnostics.ExperimentalApi)]
public virtual TBuilder UseWindowFunctionAggregates(bool useWindowFunctionAggregates = true)
=> WithOption(e => (TExtension)e.WithUseWindowFunctionAggregates(useWindowFunctionAggregates));

/// <summary>
/// Sets an option by cloning the extension used to store the settings. This ensures the builder
/// does not modify options that are already in use elsewhere.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.Diagnostics.CodeAnalysis;
using System.Text;

namespace Microsoft.EntityFrameworkCore.Infrastructure;
Expand Down Expand Up @@ -37,6 +38,7 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension
private string? _migrationsHistoryTableSchema;
private Func<ExecutionStrategyDependencies, IExecutionStrategy>? _executionStrategyFactory;
private ParameterTranslationMode? _parameterizedCollectionMode;
private bool _useWindowFunctionAggregates;

/// <summary>
/// Creates a new set of options with everything set to default values.
Expand Down Expand Up @@ -65,6 +67,7 @@ protected RelationalOptionsExtension(RelationalOptionsExtension copyFrom)
_migrationsHistoryTableSchema = copyFrom._migrationsHistoryTableSchema;
_executionStrategyFactory = copyFrom._executionStrategyFactory;
_parameterizedCollectionMode = copyFrom._parameterizedCollectionMode;
_useWindowFunctionAggregates = copyFrom._useWindowFunctionAggregates;
}

/// <summary>
Expand Down Expand Up @@ -404,6 +407,32 @@ public virtual RelationalOptionsExtension WithUseParameterizedCollectionMode(
return clone;
}

/// <summary>
/// Whether aggregates projected alongside the elements of a grouping are translated as window functions.
/// </summary>
/// <remarks>
/// See <see href="https://github.com/dotnet/efcore/issues/12747">the window function issue</see> for more information.
/// </remarks>
[Experimental(EFDiagnostics.ExperimentalApi)]
public virtual bool UseWindowFunctionAggregates
=> _useWindowFunctionAggregates;

/// <summary>
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="useWindowFunctionAggregates">The option to change.</param>
/// <returns>A new instance with the option changed.</returns>
[Experimental(EFDiagnostics.ExperimentalApi)]
public virtual RelationalOptionsExtension WithUseWindowFunctionAggregates(bool useWindowFunctionAggregates)
{
var clone = Clone();

clone._useWindowFunctionAggregates = useWindowFunctionAggregates;

return clone;
}

/// <summary>
/// Finds an existing <see cref="RelationalOptionsExtension" /> registered on the given options
/// or throws if none has been registered. This is typically used to find some relational
Expand Down Expand Up @@ -563,6 +592,11 @@ public override string LogFragment
.Append(' ');
}

if (Extension._useWindowFunctionAggregates)
{
builder.Append("UseWindowFunctionAggregates ");
}

_logFragment = builder.ToString();
}

Expand Down
52 changes: 41 additions & 11 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ protected override Expression VisitExtension(Expression expression)
UpdateExpression e => VisitUpdate(e),
JsonScalarExpression e => VisitJsonScalar(e),
ValuesExpression e => VisitValues(e),
#pragma warning disable EF9001 // Window function support is experimental
WindowFunctionExpression e => VisitWindowFunction(e),
#pragma warning restore EF9001

_ => throw new InvalidOperationException(
RelationalStrings.UnhandledExpressionInVisitor(expression, expression.GetType(), nameof(QuerySqlGenerator))),
Expand Down Expand Up @@ -1384,17 +1387,8 @@ protected virtual Expression VisitScalarSubquery(ScalarSubqueryExpression scalar
/// <param name="rowNumberExpression">The <see cref="RowNumberExpression" /> for which to generate SQL.</param>
protected virtual Expression VisitRowNumber(RowNumberExpression rowNumberExpression)
{
_relationalCommandBuilder.Append("ROW_NUMBER() OVER(");
if (rowNumberExpression.Partitions.Any())
{
_relationalCommandBuilder.Append("PARTITION BY ");
GenerateList(rowNumberExpression.Partitions, e => Visit(e));
_relationalCommandBuilder.Append(" ");
}

_relationalCommandBuilder.Append("ORDER BY ");
GenerateList(rowNumberExpression.Orderings, e => Visit(e));
_relationalCommandBuilder.Append(")");
_relationalCommandBuilder.Append("ROW_NUMBER()");
GenerateOver(rowNumberExpression.Partitions, rowNumberExpression.Orderings);

return rowNumberExpression;
}
Expand All @@ -1408,6 +1402,42 @@ private Expression VisitDenseRank(DenseRankExpression denseRankExpression)
return denseRankExpression;
}

/// <summary>
/// Generates SQL for a window function expression.
/// </summary>
/// <param name="windowFunctionExpression">The <see cref="WindowFunctionExpression" /> for which to generate SQL.</param>
[Experimental(EFDiagnostics.ExperimentalApi)]
protected virtual Expression VisitWindowFunction(WindowFunctionExpression windowFunctionExpression)
{
Visit(windowFunctionExpression.Function);
GenerateOver(windowFunctionExpression.Partitions, windowFunctionExpression.Orderings);

return windowFunctionExpression;
}

private void GenerateOver(IReadOnlyList<SqlExpression> partitions, IReadOnlyList<OrderingExpression> orderings)
{
_relationalCommandBuilder.Append(" OVER(");
if (partitions.Count > 0)
{
_relationalCommandBuilder.Append("PARTITION BY ");
GenerateList(partitions, e => Visit(e));

if (orderings.Count > 0)
{
_relationalCommandBuilder.Append(" ");
}
}

if (orderings.Count > 0)
{
_relationalCommandBuilder.Append("ORDER BY ");
GenerateList(orderings, e => Visit(e));
}

_relationalCommandBuilder.Append(")");
}

/// <summary>
/// Generates SQL for a row value expression.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.EntityFrameworkCore.Query;

/// <inheritdoc />
Expand Down Expand Up @@ -37,11 +39,14 @@ public override object GenerateCacheKey(Expression query, bool async)
{
var relationalOptions = RelationalOptionsExtension.Extract(RelationalDependencies.ContextOptions);

#pragma warning disable EF9001 // Window function support is experimental
return new RelationalCompiledQueryCacheKey(
base.GenerateCacheKeyCore(query, async),
relationalOptions.UseRelationalNulls,
relationalOptions.QuerySplittingBehavior,
shouldBuffer: ExecutionStrategy.Current?.RetriesOnFailure ?? Dependencies.IsRetryingExecutionStrategy);
shouldBuffer: ExecutionStrategy.Current?.RetriesOnFailure ?? Dependencies.IsRetryingExecutionStrategy,
relationalOptions.UseWindowFunctionAggregates);
#pragma warning restore EF9001
}

/// <summary>
Expand All @@ -60,6 +65,7 @@ public override object GenerateCacheKey(Expression query, bool async)
private readonly bool _useRelationalNulls;
private readonly QuerySplittingBehavior? _querySplittingBehavior;
private readonly bool _shouldBuffer;
private readonly bool _useWindowFunctionAggregates;

/// <summary>
/// Initializes a new instance of the <see cref="RelationalCompiledQueryCacheKey" /> class.
Expand All @@ -73,11 +79,35 @@ public RelationalCompiledQueryCacheKey(
bool useRelationalNulls,
QuerySplittingBehavior? querySplittingBehavior,
bool shouldBuffer)
#pragma warning disable EF9001 // Window function support is experimental
: this(compiledQueryCacheKey, useRelationalNulls, querySplittingBehavior, shouldBuffer, useWindowFunctionAggregates: false)
#pragma warning restore EF9001
{
}

/// <summary>
/// Initializes a new instance of the <see cref="RelationalCompiledQueryCacheKey" /> class.
/// </summary>
/// <param name="compiledQueryCacheKey">The non-relational cache key.</param>
/// <param name="useRelationalNulls">True to use relational null logic.</param>
/// <param name="querySplittingBehavior"><see cref="QuerySplittingBehavior" /> to use when loading related collections.</param>
/// <param name="shouldBuffer"><see langword="true" /> if the query should be buffered.</param>
/// <param name="useWindowFunctionAggregates">
/// <see langword="true" /> if aggregates projected alongside the elements of a grouping are translated as window functions.
/// </param>
[Experimental(EFDiagnostics.ExperimentalApi)]
public RelationalCompiledQueryCacheKey(
CompiledQueryCacheKey compiledQueryCacheKey,
bool useRelationalNulls,
QuerySplittingBehavior? querySplittingBehavior,
bool shouldBuffer,
bool useWindowFunctionAggregates)
{
_compiledQueryCacheKey = compiledQueryCacheKey;
_useRelationalNulls = useRelationalNulls;
_querySplittingBehavior = querySplittingBehavior;
_shouldBuffer = shouldBuffer;
_useWindowFunctionAggregates = useWindowFunctionAggregates;
}

/// <inheritdoc />
Expand All @@ -90,11 +120,12 @@ public bool Equals(RelationalCompiledQueryCacheKey other)
=> _compiledQueryCacheKey.Equals(other._compiledQueryCacheKey)
&& _useRelationalNulls == other._useRelationalNulls
&& _querySplittingBehavior == other._querySplittingBehavior
&& _shouldBuffer == other._shouldBuffer;
&& _shouldBuffer == other._shouldBuffer
&& _useWindowFunctionAggregates == other._useWindowFunctionAggregates;

/// <inheritdoc />
public override int GetHashCode()
=> HashCode.Combine(
_compiledQueryCacheKey, _useRelationalNulls, _querySplittingBehavior, _shouldBuffer);
_compiledQueryCacheKey, _useRelationalNulls, _querySplittingBehavior, _shouldBuffer, _useWindowFunctionAggregates);
}
}
Loading
Loading