diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 76bae79b2c8..bdf1082a24a 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Reflection; @@ -1113,6 +1113,14 @@ public static string UnsupportedOperatorForSqlExpression([CanBeNull] object node public static string VisitChildrenMustBeOverridden => GetString("VisitChildrenMustBeOverridden"); + /// + /// The query has been configured to use '{splitQueryEnumValue}' and contains a collection in the 'Select' call, which could not be split into separate query. Please remove '{splitQueryMethodName}' if applied or add '{singleQueryMethodName}' to the query. + /// + public static string UnableToSplitCollectionProjectionInSplitQuery([CanBeNull] object splitQueryEnumValue, [CanBeNull] object splitQueryMethodName, [CanBeNull] object singleQueryMethodName) + => string.Format( + GetString("UnableToSplitCollectionProjectionInSplitQuery", nameof(splitQueryEnumValue), nameof(splitQueryMethodName), nameof(singleQueryMethodName)), + splitQueryEnumValue, splitQueryMethodName, singleQueryMethodName); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index ed2e18a3359..ca162da6e51 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -1,17 +1,17 @@  - @@ -765,4 +765,7 @@ VisitChildren must be overridden in class deriving from SqlExpression. + + The query has been configured to use '{splitQueryEnumValue}' and contains a collection in the 'Select' call, which could not be split into separate query. Please remove '{splitQueryMethodName}' if applied or add '{singleQueryMethodName}' to the query. + \ No newline at end of file diff --git a/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs index 34c4d1d1bc3..6d8617c0252 100644 --- a/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/CollectionJoinApplyingExpressionVisitor.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -68,36 +69,26 @@ protected override Expression VisitExtension(Expression extensionExpression) selectExpression.PushdownIntoSubquery(); } - if (_splitQuery) - { - var splitCollectionShaperExpression = (RelationalSplitCollectionShaperExpression)selectExpression.ApplyCollectionJoin( - projectionBindingExpression.Index.Value, - collectionId, - collectionShaperExpression.InnerShaper, - collectionShaperExpression.Navigation, - collectionShaperExpression.ElementType, - _splitQuery); + var innerShaper = Visit(collectionShaperExpression.InnerShaper); - var innerShaper = Visit(splitCollectionShaperExpression.InnerShaper); + var collectionJoin = selectExpression.ApplyCollectionJoin( + projectionBindingExpression.Index.Value, + collectionId, + innerShaper, + collectionShaperExpression.Navigation, + collectionShaperExpression.ElementType, + _splitQuery); - return splitCollectionShaperExpression.Update( - splitCollectionShaperExpression.ParentIdentifier, - splitCollectionShaperExpression.ChildIdentifier, - splitCollectionShaperExpression.SelectExpression, - innerShaper); - } - else + if (_splitQuery + && collectionJoin == null) { - var innerShaper = Visit(collectionShaperExpression.InnerShaper); - - return selectExpression.ApplyCollectionJoin( - projectionBindingExpression.Index.Value, - collectionId, - innerShaper, - collectionShaperExpression.Navigation, - collectionShaperExpression.ElementType, - _splitQuery); + throw new InvalidOperationException(RelationalStrings.UnableToSplitCollectionProjectionInSplitQuery( + $"{nameof(QuerySplittingBehavior)}.{QuerySplittingBehavior.SplitQuery}", + nameof(RelationalQueryableExtensions.AsSplitQuery), + nameof(RelationalQueryableExtensions.AsSingleQuery))); } + + return collectionJoin; } return extensionExpression is ShapedQueryExpression shapedQueryExpression diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 2dd9808b9ee..37b741822a2 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1330,6 +1330,13 @@ public Expression ApplyCollectionJoin( if (splitQuery) { + var containsReferenceToOuter = new SelectExpressionCorrelationFindingExpressionVisitor(this) + .ContainsOuterReference(innerSelectExpression); + if (containsReferenceToOuter) + { + return null; + } + var parentIdentifier = GetIdentifierAccessor(_identifier).Item1; innerSelectExpression.ApplyProjection(); diff --git a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalTestBase.cs index ce923b09bd4..7fccc22d82b 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/ComplexNavigationsQueryRelationalTestBase.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -482,6 +483,51 @@ public virtual async Task Filtered_include_calling_methods_directly_on_parameter .AsSplitQuery()))).Message; } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Projecting_collection_with_FirstOrDefault_split_throws(bool async) + { + Assert.Equal( + RelationalStrings.UnableToSplitCollectionProjectionInSplitQuery( + "QuerySplittingBehavior.SplitQuery", "AsSplitQuery", "AsSingleQuery"), + (await Assert.ThrowsAsync( + () => AssertFirstOrDefault( + async, + ss => ss.Set() + .AsSplitQuery() + .Select(e => new + { + e.Id, + Level2s = e.OneToMany_Optional1.ToList() + }), + predicate: l => l.Id == 1, + asserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + AssertCollection(e.Level2s, a.Level2s); + }))).Message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Projecting_collection_with_FirstOrDefault_without_split_works(bool async) + { + return AssertFirstOrDefault( + async, + ss => ss.Set() + .Select(e => new + { + e.Id, + Level2s = e.OneToMany_Optional1.ToList() + }), + predicate: l => l.Id == 1, + asserter: (e, a) => + { + Assert.Equal(e.Id, a.Id); + AssertCollection(e.Level2s, a.Level2s); + }); + } + protected virtual bool CanExecuteQueryString => false; protected override QueryAsserter CreateQueryAsserter(TFixture fixture) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 05e0837d1d8..ac099482ab0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -5087,7 +5087,7 @@ FROM [LevelTwo] AS [l0] ) AS [t] WHERE 0 < [t].[row] ) AS [t0] ON [l].[Id] = [t0].[OneToMany_Optional_Inverse2Id] -ORDER BY [l].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name], [t0].[Id]", +ORDER BY [l].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name]", // @"SELECT [t2].[Id], [t2].[Level2_Optional_Id], [t2].[Level2_Required_Id], [t2].[Name], [t2].[OneToMany_Optional_Inverse3Id], [t2].[OneToMany_Optional_Self_Inverse3Id], [t2].[OneToMany_Required_Inverse3Id], [t2].[OneToMany_Required_Self_Inverse3Id], [t2].[OneToOne_Optional_PK_Inverse3Id], [t2].[OneToOne_Optional_Self3Id], [l].[Id], [t0].[Id] FROM [LevelOne] AS [l] @@ -5107,7 +5107,7 @@ FROM [LevelThree] AS [l1] ) AS [t1] WHERE 0 < [t1].[row] ) AS [t2] ON [t0].[Id] = [t2].[OneToMany_Optional_Inverse3Id] -ORDER BY [l].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name], [t0].[Id], [t2].[OneToMany_Optional_Inverse3Id], [t2].[Name] DESC"); +ORDER BY [l].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name], [t2].[OneToMany_Optional_Inverse3Id], [t2].[Name] DESC"); } public override async Task Filtered_include_basic_OrderBy_Take_split(bool async) @@ -5326,7 +5326,7 @@ FROM [LevelTwo] AS [l0] ) AS [t] WHERE [t].[row] <= 3 ) AS [t0] ON [l].[Id] = [t0].[OneToMany_Optional_Inverse2Id] -ORDER BY [l].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name], [t0].[Id]", +ORDER BY [l].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name]", // @"SELECT [t2].[Id], [t2].[Level2_Optional_Id], [t2].[Level2_Required_Id], [t2].[Name], [t2].[OneToMany_Optional_Inverse3Id], [t2].[OneToMany_Optional_Self_Inverse3Id], [t2].[OneToMany_Required_Inverse3Id], [t2].[OneToMany_Required_Self_Inverse3Id], [t2].[OneToOne_Optional_PK_Inverse3Id], [t2].[OneToOne_Optional_Self3Id], [l].[Id], [t0].[Id] FROM [LevelOne] AS [l] @@ -5348,7 +5348,7 @@ FROM [LevelThree] AS [l1] ) AS [t1] WHERE 1 < [t1].[row] ) AS [t2] ON [t0].[Id] = [t2].[OneToMany_Required_Inverse3Id] -ORDER BY [l].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name], [t0].[Id], [t2].[OneToMany_Required_Inverse3Id], [t2].[Name] DESC"); +ORDER BY [l].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse2Id], [t0].[Name], [t2].[OneToMany_Required_Inverse3Id], [t2].[Name] DESC"); } public override async Task Filtered_include_same_filter_set_on_same_navigation_twice_split(bool async) @@ -5567,7 +5567,7 @@ FROM [LevelThree] AS [l1] ) AS [t] WHERE [t].[row] <= 1 ) AS [t0] ON [l0].[Id] = [t0].[OneToMany_Optional_Inverse3Id] -ORDER BY [l].[Id], [l0].[Id], [t0].[OneToMany_Optional_Inverse3Id], [t0].[Id]", +ORDER BY [l].[Id], [l0].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse3Id]", // @"SELECT [l2].[Id], [l2].[Level3_Optional_Id], [l2].[Level3_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_Inverse4Id], [l2].[OneToMany_Optional_Self_Inverse4Id], [l2].[OneToMany_Required_Inverse4Id], [l2].[OneToMany_Required_Self_Inverse4Id], [l2].[OneToOne_Optional_PK_Inverse4Id], [l2].[OneToOne_Optional_Self4Id], [l].[Id], [l0].[Id], [t0].[Id] FROM [LevelOne] AS [l] @@ -5582,7 +5582,7 @@ FROM [LevelThree] AS [l1] WHERE [t].[row] <= 1 ) AS [t0] ON [l0].[Id] = [t0].[OneToMany_Optional_Inverse3Id] INNER JOIN [LevelFour] AS [l2] ON [t0].[Id] = [l2].[OneToMany_Optional_Inverse4Id] -ORDER BY [l].[Id], [l0].[Id], [t0].[OneToMany_Optional_Inverse3Id], [t0].[Id]", +ORDER BY [l].[Id], [l0].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse3Id]", // @"SELECT [l2].[Id], [l2].[Level3_Optional_Id], [l2].[Level3_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_Inverse4Id], [l2].[OneToMany_Optional_Self_Inverse4Id], [l2].[OneToMany_Required_Inverse4Id], [l2].[OneToMany_Required_Self_Inverse4Id], [l2].[OneToOne_Optional_PK_Inverse4Id], [l2].[OneToOne_Optional_Self4Id], [l].[Id], [l0].[Id], [t0].[Id] FROM [LevelOne] AS [l] @@ -5597,7 +5597,7 @@ FROM [LevelThree] AS [l1] WHERE [t].[row] <= 1 ) AS [t0] ON [l0].[Id] = [t0].[OneToMany_Optional_Inverse3Id] INNER JOIN [LevelFour] AS [l2] ON [t0].[Id] = [l2].[OneToMany_Required_Inverse4Id] -ORDER BY [l].[Id], [l0].[Id], [t0].[OneToMany_Optional_Inverse3Id], [t0].[Id]"); +ORDER BY [l].[Id], [l0].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse3Id]"); } public override async Task Filtered_include_complex_three_level_with_middle_having_filter2_split(bool async) @@ -5626,7 +5626,7 @@ FROM [LevelThree] AS [l1] ) AS [t] WHERE [t].[row] <= 1 ) AS [t0] ON [l0].[Id] = [t0].[OneToMany_Optional_Inverse3Id] -ORDER BY [l].[Id], [l0].[Id], [t0].[OneToMany_Optional_Inverse3Id], [t0].[Id]", +ORDER BY [l].[Id], [l0].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse3Id]", // @"SELECT [l2].[Id], [l2].[Level3_Optional_Id], [l2].[Level3_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_Inverse4Id], [l2].[OneToMany_Optional_Self_Inverse4Id], [l2].[OneToMany_Required_Inverse4Id], [l2].[OneToMany_Required_Self_Inverse4Id], [l2].[OneToOne_Optional_PK_Inverse4Id], [l2].[OneToOne_Optional_Self4Id], [l].[Id], [l0].[Id], [t0].[Id] FROM [LevelOne] AS [l] @@ -5641,7 +5641,7 @@ FROM [LevelThree] AS [l1] WHERE [t].[row] <= 1 ) AS [t0] ON [l0].[Id] = [t0].[OneToMany_Optional_Inverse3Id] INNER JOIN [LevelFour] AS [l2] ON [t0].[Id] = [l2].[OneToMany_Optional_Inverse4Id] -ORDER BY [l].[Id], [l0].[Id], [t0].[OneToMany_Optional_Inverse3Id], [t0].[Id]", +ORDER BY [l].[Id], [l0].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse3Id]", // @"SELECT [l2].[Id], [l2].[Level3_Optional_Id], [l2].[Level3_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_Inverse4Id], [l2].[OneToMany_Optional_Self_Inverse4Id], [l2].[OneToMany_Required_Inverse4Id], [l2].[OneToMany_Required_Self_Inverse4Id], [l2].[OneToOne_Optional_PK_Inverse4Id], [l2].[OneToOne_Optional_Self4Id], [l].[Id], [l0].[Id], [t0].[Id] FROM [LevelOne] AS [l] @@ -5656,7 +5656,7 @@ FROM [LevelThree] AS [l1] WHERE [t].[row] <= 1 ) AS [t0] ON [l0].[Id] = [t0].[OneToMany_Optional_Inverse3Id] INNER JOIN [LevelFour] AS [l2] ON [t0].[Id] = [l2].[OneToMany_Required_Inverse4Id] -ORDER BY [l].[Id], [l0].[Id], [t0].[OneToMany_Optional_Inverse3Id], [t0].[Id]"); +ORDER BY [l].[Id], [l0].[Id], [t0].[Id], [t0].[OneToMany_Optional_Inverse3Id]"); } public override void Filtered_include_variable_used_inside_filter_split() @@ -5833,6 +5833,21 @@ ELSE [l1].[Name] END IS NULL"); } + public override async Task Projecting_collection_with_FirstOrDefault_without_split_works(bool async) + { + await base.Projecting_collection_with_FirstOrDefault_without_split_works(async); + + AssertSql( + @"SELECT [t].[Id], [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] +FROM ( + SELECT TOP(1) [l].[Id] + FROM [LevelOne] AS [l] + WHERE [l].[Id] = 1 +) AS [t] +LEFT JOIN [LevelTwo] AS [l0] ON [t].[Id] = [l0].[OneToMany_Optional_Inverse2Id] +ORDER BY [t].[Id], [l0].[Id]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyNoTrackingQuerySqlServerTest.cs index ae66ab610e4..6f81ef47120 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyNoTrackingQuerySqlServerTest.cs @@ -1348,7 +1348,7 @@ FROM [JoinOneToTwo] AS [j] ) AS [t] WHERE (1 < [t].[row]) AND ([t].[row] <= 3) ) AS [t0] ON [e].[Id] = [t0].[OneId] -ORDER BY [e].[Id], [t0].[OneId], [t0].[Id], [t0].[TwoId]", +ORDER BY [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id]", // @"SELECT [t1].[Id], [t1].[CollectionInverseId], [t1].[Name], [t1].[ReferenceInverseId], [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id] FROM [EntityOnes] AS [e] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs index 594c7a5a529..a9c8e766841 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs @@ -1348,7 +1348,7 @@ FROM [JoinOneToTwo] AS [j] ) AS [t] WHERE (1 < [t].[row]) AND ([t].[row] <= 3) ) AS [t0] ON [e].[Id] = [t0].[OneId] -ORDER BY [e].[Id], [t0].[OneId], [t0].[Id], [t0].[TwoId]", +ORDER BY [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id]", // @"SELECT [t1].[ThreeId], [t1].[TwoId], [t1].[Id], [t1].[CollectionInverseId], [t1].[Name], [t1].[ReferenceInverseId], [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id] FROM [EntityOnes] AS [e] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyNoTrackingQuerySqlServerTest.cs index c980f642f3a..42b38d6c332 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyNoTrackingQuerySqlServerTest.cs @@ -1420,7 +1420,7 @@ FROM [JoinOneToTwo] AS [j] ) AS [t] WHERE (1 < [t].[row]) AND ([t].[row] <= 3) ) AS [t0] ON [e].[Id] = [t0].[OneId] -ORDER BY [e].[Id], [t0].[OneId], [t0].[Id], [t0].[TwoId]", +ORDER BY [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id]", // @"SELECT [t1].[Id], [t1].[CollectionInverseId], [t1].[Name], [t1].[ReferenceInverseId], [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id] FROM [EntityOnes] AS [e] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyQuerySqlServerTest.cs index c94d56aba0a..09bd5ec8014 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTManyToManyQuerySqlServerTest.cs @@ -1420,7 +1420,7 @@ FROM [JoinOneToTwo] AS [j] ) AS [t] WHERE (1 < [t].[row]) AND ([t].[row] <= 3) ) AS [t0] ON [e].[Id] = [t0].[OneId] -ORDER BY [e].[Id], [t0].[OneId], [t0].[Id], [t0].[TwoId]", +ORDER BY [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id]", // @"SELECT [t1].[ThreeId], [t1].[TwoId], [t1].[Id], [t1].[CollectionInverseId], [t1].[Name], [t1].[ReferenceInverseId], [e].[Id], [t0].[OneId], [t0].[TwoId], [t0].[Id] FROM [EntityOnes] AS [e]