Skip to content

Commit 883bcc1

Browse files
committed
Relational: Implement split query for non-include collections
Resolves #21234 Resolves #22283 Resolves #23803
1 parent 438b05f commit 883bcc1

26 files changed

+2434
-922
lines changed

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 0 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -748,9 +748,6 @@
748748
<data name="UnableToBindMemberToEntityProjection" xml:space="preserve">
749749
<value>Unable to bind '{memberType}.{member}' to an entity projection of '{entityType}'.</value>
750750
</data>
751-
<data name="UnableToSplitCollectionProjectionInSplitQuery" xml:space="preserve">
752-
<value>The query has been configured to use '{splitQueryEnumValue}', but contains a collection in the 'Select' call which could not be split into a separate query. Remove '{splitQueryMethodName}' if applied, or add '{singleQueryMethodName}' to the query.</value>
753-
</data>
754751
<data name="UnhandledExpressionInVisitor" xml:space="preserve">
755752
<value>Unhandled expression '{expression}' of type '{expressionType}' encountered in '{visitor}'.</value>
756753
</data>

src/EFCore.Relational/Query/Internal/SplitIncludeRewritingExpressionVisitor.cs

Lines changed: 0 additions & 198 deletions
This file was deleted.

src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,5 @@ public override Expression NormalizeQueryableMethod(Expression expression)
4444

4545
return expression;
4646
}
47-
48-
/// <inheritdoc />
49-
public override Expression Process(Expression query)
50-
{
51-
query = base.Process(query);
52-
53-
return _relationalQueryCompilationContext.QuerySplittingBehavior == QuerySplittingBehavior.SplitQuery
54-
? new SplitIncludeRewritingExpressionVisitor().Visit(query)
55-
: query;
56-
}
5747
}
5848
}

src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions
1717
/// not used in application code.
1818
/// </para>
1919
/// </summary>
20-
public class FromSqlExpression : TableExpressionBase
20+
public class FromSqlExpression : TableExpressionBase, ICloneable
2121
{
2222
/// <summary>
2323
/// Creates a new instance of the <see cref="FromSqlExpression" /> class.
@@ -95,6 +95,9 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
9595
return this;
9696
}
9797

98+
/// <inheritdoc />
99+
public virtual object Clone() => new FromSqlExpression(Alias, Sql, Arguments);
100+
98101
/// <inheritdoc />
99102
protected override void Print(ExpressionPrinter expressionPrinter)
100103
{

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -712,11 +712,15 @@ public SelectExpressionVerifyingExpressionVisitor(IEnumerable<TableReferenceExpr
712712
Visit(childIdentifier.Column);
713713
}
714714

715-
break;
715+
return selectExpression;
716716

717717
case ConcreteColumnExpression concreteColumnExpression:
718718
concreteColumnExpression.Verify(_tableReferencesInScope);
719-
break;
719+
return concreteColumnExpression;
720+
721+
case ShapedQueryExpression shapedQueryExpression:
722+
Verify(shapedQueryExpression.QueryExpression, _tableReferencesInScope);
723+
return shapedQueryExpression;
720724
}
721725

722726
return base.Visit(expression);
@@ -727,5 +731,95 @@ public static void Verify(Expression expression, IEnumerable<TableReferenceExpre
727731
=> new SelectExpressionVerifyingExpressionVisitor(tableReferencesInScope)
728732
.Visit(expression);
729733
}
734+
735+
private sealed class CloningExpressionVisitor : ExpressionVisitor
736+
{
737+
[return: NotNullIfNotNull("expression")]
738+
public override Expression? Visit(Expression? expression)
739+
{
740+
if (expression is SelectExpression selectExpression)
741+
{
742+
// We ignore projection binding related elements as we don't want to copy them over for top level
743+
// Nested level will have _projection populated and no binding elements
744+
var newProjections = selectExpression._projection.Select(Visit).ToList<ProjectionExpression>();
745+
746+
var newTables = selectExpression._tables.Select(Visit).ToList<TableExpressionBase>();
747+
// Since we are cloning we need to generate new table references
748+
// In other cases (like VisitChildren), we just reuse the same table references and update the SelectExpression inside it.
749+
// We initially assign old SelectExpression in table references and later update it once we construct clone
750+
var newTableReferences = selectExpression._tableReferences
751+
.Select(e => new TableReferenceExpression(selectExpression, e.Alias)).ToList();
752+
Check.DebugAssert(
753+
newTables.Select(e => GetAliasFromTableExpressionBase(e)).SequenceEqual(newTableReferences.Select(e => e.Alias)),
754+
"Alias of updated tables must match the old tables.");
755+
756+
var predicate = (SqlExpression?)Visit(selectExpression.Predicate);
757+
var newGroupBy = selectExpression._groupBy.Select(Visit)
758+
.Where(e => !(e is SqlConstantExpression || e is SqlParameterExpression))
759+
.ToList<SqlExpression>();
760+
var havingExpression = (SqlExpression?)Visit(selectExpression.Having);
761+
var newOrderings = selectExpression._orderings.Select(Visit).ToList<OrderingExpression>();
762+
var offset = (SqlExpression?)Visit(selectExpression.Offset);
763+
var limit = (SqlExpression?)Visit(selectExpression.Limit);
764+
765+
var newSelectExpression = new SelectExpression(selectExpression.Alias, newProjections, newTables, newTableReferences, newGroupBy, newOrderings)
766+
{
767+
Predicate = predicate,
768+
Having = havingExpression,
769+
Offset = offset,
770+
Limit = limit,
771+
IsDistinct = selectExpression.IsDistinct,
772+
Tags = selectExpression.Tags,
773+
_usedAliases = selectExpression._usedAliases.ToHashSet()
774+
};
775+
776+
newSelectExpression._tptLeftJoinTables.AddRange(selectExpression._tptLeftJoinTables);
777+
// Since identifiers are ColumnExpression, they are not visited since they don't contain SelectExpression inside it.
778+
newSelectExpression._identifier.AddRange(selectExpression._identifier);
779+
newSelectExpression._childIdentifiers.AddRange(selectExpression._childIdentifiers);
780+
781+
// Remap tableReferences in new select expression
782+
foreach (var tableReference in newTableReferences)
783+
{
784+
tableReference.UpdateTableReference(selectExpression, newSelectExpression);
785+
}
786+
787+
// Now that we have SelectExpression, we visit all components and update table references inside columns
788+
newSelectExpression = (SelectExpression)new ColumnExpressionReplacingExpressionVisitor(selectExpression, newSelectExpression)
789+
.Visit(newSelectExpression);
790+
791+
return newSelectExpression;
792+
793+
}
794+
795+
return expression is ICloneable cloneable ? (Expression)cloneable.Clone() : base.Visit(expression);
796+
}
797+
}
798+
799+
private sealed class ColumnExpressionReplacingExpressionVisitor : ExpressionVisitor
800+
{
801+
private readonly SelectExpression _oldSelectExpression;
802+
private readonly Dictionary<string, TableReferenceExpression> _newTableReferences;
803+
804+
public ColumnExpressionReplacingExpressionVisitor(SelectExpression oldSelectExpression, SelectExpression newSelectExpression)
805+
{
806+
_oldSelectExpression = oldSelectExpression;
807+
_newTableReferences = newSelectExpression._tableReferences.ToDictionary(e => e.Alias);
808+
}
809+
810+
[return: NotNullIfNotNull("expression")]
811+
public override Expression? Visit(Expression? expression)
812+
{
813+
return expression is ConcreteColumnExpression concreteColumnExpression
814+
&& _oldSelectExpression.ContainsTableReference(concreteColumnExpression)
815+
? new ConcreteColumnExpression(
816+
concreteColumnExpression.Name,
817+
_newTableReferences[concreteColumnExpression.TableAlias],
818+
concreteColumnExpression.Type,
819+
concreteColumnExpression.TypeMapping!,
820+
concreteColumnExpression.IsNullable)
821+
: base.Visit(expression);
822+
}
823+
}
730824
}
731825
}

0 commit comments

Comments
 (0)