-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Redo SQL table alias management #32785
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.EntityFrameworkCore.Query; | ||
|
|
||
| /// <summary> | ||
| /// A factory creating managers for SQL aliases, capable of generate uniquified table aliases. | ||
| /// </summary> | ||
| public interface ISqlAliasManagerFactory | ||
| { | ||
| /// <summary> | ||
| /// Creates a new <see cref="SqlAliasManager" />. | ||
| /// </summary> | ||
| SqlAliasManager Create(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.EntityFrameworkCore.Query.Internal; | ||
|
|
||
| /// <summary> | ||
| /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
| /// the same compatibility standards as public APIs. It may be changed or removed without notice in | ||
| /// any release. You should only use it directly in your code with extreme caution and knowing that | ||
| /// doing so can result in application failures when updating to a new Entity Framework Core release. | ||
| /// </summary> | ||
| public class SqlAliasManagerFactory : ISqlAliasManagerFactory | ||
| { | ||
| /// <summary> | ||
| /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | ||
| /// the same compatibility standards as public APIs. It may be changed or removed without notice in | ||
| /// any release. You should only use it directly in your code with extreme caution and knowing that | ||
| /// doing so can result in application failures when updating to a new Entity Framework Core release. | ||
| /// </summary> | ||
| public SqlAliasManager Create() | ||
| => new(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Query; | |
| public class RelationalQueryTranslationPostprocessor : QueryTranslationPostprocessor | ||
| { | ||
| private readonly SqlTreePruner _pruner = new(); | ||
| private readonly SqlAliasManager _sqlAliasManager; | ||
| private readonly bool _useRelationalNulls; | ||
|
|
||
| /// <summary> | ||
|
|
@@ -22,10 +23,11 @@ public class RelationalQueryTranslationPostprocessor : QueryTranslationPostproce | |
| public RelationalQueryTranslationPostprocessor( | ||
| QueryTranslationPostprocessorDependencies dependencies, | ||
| RelationalQueryTranslationPostprocessorDependencies relationalDependencies, | ||
| QueryCompilationContext queryCompilationContext) | ||
| RelationalQueryCompilationContext queryCompilationContext) | ||
| : base(dependencies, queryCompilationContext) | ||
| { | ||
| RelationalDependencies = relationalDependencies; | ||
| _sqlAliasManager = queryCompilationContext.SqlAliasManager; | ||
| _useRelationalNulls = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).UseRelationalNulls; | ||
| } | ||
|
|
||
|
|
@@ -37,24 +39,24 @@ public RelationalQueryTranslationPostprocessor( | |
| /// <inheritdoc /> | ||
| public override Expression Process(Expression query) | ||
| { | ||
| query = base.Process(query); | ||
| query = new SelectExpressionProjectionApplyingExpressionVisitor( | ||
| ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior).Visit(query); | ||
| query = Prune(query); | ||
| var query1 = base.Process(query); | ||
| var query2 = new SelectExpressionProjectionApplyingExpressionVisitor( | ||
| ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior).Visit(query1); | ||
| var query3 = Prune(query2); | ||
|
|
||
| // TODO: This - and all the verifications below - should happen after all visitors have run, including provider-specific ones. | ||
| var query4 = _sqlAliasManager.PostprocessAliases(query3); | ||
|
|
||
| #if DEBUG | ||
| // Verifies that all SelectExpression are marked as immutable after this point. | ||
| new SelectExpressionMutableVerifyingExpressionVisitor().Visit(query); | ||
| // Verifies that all table aliases are uniquely assigned without skipping over | ||
| // Which points to possible mutation of a SelectExpression being used in multiple places. | ||
| new TableAliasVerifyingExpressionVisitor().Visit(query); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This verification visitor no longer makes sense, but I'll be introducing something better to check correct table/column scoping, after TableReferenceExpression is gone. |
||
| new SelectExpressionMutableVerifyingExpressionVisitor().Visit(query4); | ||
| #endif | ||
|
|
||
| query = new SqlExpressionSimplifyingExpressionVisitor(RelationalDependencies.SqlExpressionFactory, _useRelationalNulls) | ||
| .Visit(query); | ||
| query = new RelationalValueConverterCompensatingExpressionVisitor(RelationalDependencies.SqlExpressionFactory).Visit(query); | ||
| var query5 = new SqlExpressionSimplifyingExpressionVisitor(RelationalDependencies.SqlExpressionFactory, _useRelationalNulls) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to make it easier to debug and compare multiple versions of the tree in the various phases. |
||
| .Visit(query4); | ||
| var query6 = new RelationalValueConverterCompensatingExpressionVisitor(RelationalDependencies.SqlExpressionFactory).Visit(query5); | ||
|
|
||
| return query; | ||
| return query6; | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -85,99 +87,5 @@ private sealed class SelectExpressionMutableVerifyingExpressionVisitor : Express | |
| } | ||
| } | ||
| } | ||
|
|
||
| private sealed class TableAliasVerifyingExpressionVisitor : ExpressionVisitor | ||
| { | ||
| private readonly ScopedVisitor _scopedVisitor = new(); | ||
|
|
||
| // Validates that all aliases are unique inside SelectExpression | ||
| // And all aliases are used in without any generated alias being missing | ||
| [return: NotNullIfNotNull("expression")] | ||
| public override Expression? Visit(Expression? expression) | ||
| { | ||
| switch (expression) | ||
| { | ||
| case ShapedQueryExpression shapedQueryExpression: | ||
| VerifyUniqueAliasInExpression(shapedQueryExpression.QueryExpression); | ||
| Visit(shapedQueryExpression.QueryExpression); | ||
| return shapedQueryExpression; | ||
|
|
||
| case RelationalSplitCollectionShaperExpression relationalSplitCollectionShaperExpression: | ||
| VerifyUniqueAliasInExpression(relationalSplitCollectionShaperExpression.SelectExpression); | ||
| Visit(relationalSplitCollectionShaperExpression.InnerShaper); | ||
| return relationalSplitCollectionShaperExpression; | ||
|
|
||
| case NonQueryExpression nonQueryExpression: | ||
| VerifyUniqueAliasInExpression(nonQueryExpression.Expression); | ||
| return nonQueryExpression; | ||
|
|
||
| default: | ||
| return base.Visit(expression); | ||
| } | ||
| } | ||
|
|
||
| private void VerifyUniqueAliasInExpression(Expression expression) | ||
| => _scopedVisitor.EntryPoint(expression); | ||
|
|
||
| private sealed class ScopedVisitor : ExpressionVisitor | ||
| { | ||
| private readonly HashSet<string> _usedAliases = new(StringComparer.OrdinalIgnoreCase); | ||
| private readonly HashSet<TableExpressionBase> _visitedTableExpressionBases = new(ReferenceEqualityComparer.Instance); | ||
|
|
||
| public Expression EntryPoint(Expression expression) | ||
| { | ||
| _usedAliases.Clear(); | ||
| _visitedTableExpressionBases.Clear(); | ||
|
|
||
| if (expression is SelectExpression selectExpression) | ||
| { | ||
| Check.DebugAssert(selectExpression.RemovedAliases is not null, "RemovedAliases not set"); | ||
| foreach (var alias in selectExpression.RemovedAliases) | ||
| { | ||
| _usedAliases.Add(alias); | ||
| } | ||
| } | ||
|
|
||
| var result = Visit(expression); | ||
|
|
||
| foreach (var group in _usedAliases.GroupBy(e => e[..1])) | ||
| { | ||
| if (group.Count() == 1) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var numbers = group.OrderBy(e => e).Skip(1).Select(e => int.Parse(e[1..])).OrderBy(e => e).ToList(); | ||
| if (numbers.Count - 1 != numbers[^1]) | ||
| { | ||
| throw new InvalidOperationException($"Missing alias in the list: {string.Join(",", group.Select(e => e))}"); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| [return: NotNullIfNotNull("expression")] | ||
| public override Expression? Visit(Expression? expression) | ||
| { | ||
| var visitedExpression = base.Visit(expression); | ||
| if (visitedExpression is TableExpressionBase tableExpressionBase | ||
| && !_visitedTableExpressionBases.Contains(tableExpressionBase) | ||
| && tableExpressionBase.Alias != null) | ||
| { | ||
| if (_usedAliases.Contains(tableExpressionBase.Alias)) | ||
| { | ||
| throw new InvalidOperationException($"Duplicate alias: {tableExpressionBase.Alias}"); | ||
| } | ||
|
|
||
| _usedAliases.Add(tableExpressionBase.Alias); | ||
|
|
||
| _visitedTableExpressionBases.Add(tableExpressionBase); | ||
| } | ||
|
|
||
| return visitedExpression; | ||
| } | ||
| } | ||
| } | ||
| #endif | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the longer-term plan, these are temporary - Select shouldn't be constructing tables within itself as it's doing now, but those tables should rather be constructed outside, i.e. in RelationalQueryableMethodTranslatingEV.