From d56c517b6e94302bd91160234236cf8632f44cd8 Mon Sep 17 00:00:00 2001 From: maumar Date: Mon, 22 Feb 2021 12:39:32 -0800 Subject: [PATCH] Fix to #22049 - Query: consider updating select expression identifiers when projecting a subset of column and adding distinct As fix to #15873 we started blocking some scenarios that used to work (by accident) - when we have a subquery using Distinct or GroupBy that doesn't happen to have any duplicates. Fix is to enable those scenarios (and others) by modifying identifier columns in case of distinct and group by, if the original identifiers are not already present. In case of distinct, the entire projection becomes unique identifier, as distinct guarantees it to be unique. In case of groupby, the grouping key becomes the identifier - since we only support grouping key or group aggregate in the projection, we are also guaranteed to have 1 row per unique grouping key. Also fix to #24288 - Query: add collection join tries to convert correlated collection from APPLY to JOIN for subqueries with Distinct and GroupBy, which is incorrect we would always try to convert subquery with groupby and distinct from apply to join, however we can only do this if the projection already contains the join key. Otherwise, adding the join key to the projection would change the meaning of operation in case of distinct and create invalid query in case of group by (projecting column that is not part of grouping key or aggregate). Fixes #22049 Fixes #24288 --- .../Properties/RelationalStrings.Designer.cs | 20 +- .../Properties/RelationalStrings.resx | 10 +- ...yableMethodTranslatingExpressionVisitor.cs | 22 ++ .../Query/SqlExpressions/SelectExpression.cs | 128 +++++++- .../NorthwindMiscellaneousQueryCosmosTest.cs | 18 ++ .../Query/NorthwindSelectQueryCosmosTest.cs | 30 +- ...NorthwindMiscellaneousQueryInMemoryTest.cs | 4 + .../GearsOfWarQueryRelationalTestBase.cs | 127 ++++++-- ...NorthwindGroupByQueryRelationalTestBase.cs | 50 +-- .../NorthwindSelectQueryRelationalTestBase.cs | 35 ++ .../Query/GearsOfWarQueryTestBase.cs | 305 ++++++++++++++++++ .../Query/NorthwindGroupByQueryTestBase.cs | 27 ++ .../NorthwindMiscellaneousQueryTestBase.cs | 81 +++++ .../Query/NorthwindSelectQueryTestBase.cs | 116 ++++++- ...NavigationsSharedTypeQuerySqlServerTest.cs | 21 +- .../Query/GearsOfWarQuerySqlServerTest.cs | 124 +++++++ .../NorthwindGroupByQuerySqlServerTest.cs | 25 ++ ...orthwindMiscellaneousQuerySqlServerTest.cs | 52 +++ .../NorthwindSelectQuerySqlServerTest.cs | 100 +++++- ...lexNavigationsSharedTypeQuerySqliteTest.cs | 10 +- .../Query/GearsOfWarQuerySqliteTest.cs | 36 +++ .../Query/NorthwindGroupByQuerySqliteTest.cs | 6 + .../NorthwindMiscellaneousQuerySqliteTest.cs | 12 + .../Query/NorthwindSelectQuerySqliteTest.cs | 30 +- .../Query/TPTGearsOfWarQuerySqliteTest.cs | 36 +++ 25 files changed, 1312 insertions(+), 113 deletions(-) diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 146a5e1398a..b50194e5f8b 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -769,13 +769,27 @@ public static string MissingConcurrencyColumn([CanBeNull] object? entityType, [C entityType, missingColumn, table); /// - /// Unable to translate a collection subquery in a projection since it uses 'Distinct' or 'Group By' operations and doesn't project key columns of all tables required to generate results on the client side. Missing column: {column}. Either add column(s) to the projection or rewrite the query to not use the 'GroupBy'/'Distinct' operation. + /// Subquery with 'Distinct' can only be translated if projection consists only of entities and their properties, or it contains keys of all entities required to generate results on the client side. Either add '{column}' to the projection, remove complex elements of the projection, or rewrite the query to not use the 'Distinct' operation. /// - public static string MissingIdentifyingProjectionInDistinctGroupBySubquery([CanBeNull] object? column) + public static string UnableToTranslateSubqueryWithDistinct([CanBeNull] object? column) => string.Format( - GetString("MissingIdentifyingProjectionInDistinctGroupBySubquery", nameof(column)), + GetString("UnableToTranslateSubqueryWithDistinct", nameof(column)), column); + /// + /// Subquery with 'GroupBy' can only be translated if grouping key consists only of entities and their properties, or it contains keys of all entities required to generate results on the client side. Either add '{column}' to the grouping key, remove complex elements of the grouping key, or rewrite the query to not use the 'GroupBy' operation. + /// + public static string UnableToTranslateSubqueryWithGroupBy([CanBeNull] object? column) + => string.Format( + GetString("UnableToTranslateSubqueryWithGroupBy", nameof(column)), + column); + + /// + /// Using 'Distinct' operation on a projection containing a collection is not supported. + /// + public static string DistinctOnCollectionNotSupported + => GetString("DistinctOnCollectionNotSupported"); + /// /// 'Reverse' could not be translated to the server because there is no ordering on the server side. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 21e27da5fc0..dc330860c94 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -621,8 +621,14 @@ Entity type '{entityType}' doesn't contain a property mapped to the store-generated concurrency token column '{missingColumn}' which is used by another entity type sharing the table '{table}'. Add a store-generated property to '{entityType}' which is mapped to the same column; it may be in shadow state. - - Unable to translate a collection subquery in a projection since it uses 'Distinct' or 'Group By' operations and doesn't project key columns of all tables required to generate results on the client side. Missing column: {column}. Either add column(s) to the projection or rewrite the query to not use the 'GroupBy'/'Distinct' operation. + + Subquery with 'Distinct' can only be translated if projection consists only of entities and their properties, or it contains keys of all entities required to generate results on the client side. Either add '{column}' to the projection, remove complex elements of the projection, or rewrite the query to not use the 'Distinct' operation. + + + Subquery with 'GroupBy' can only be translated if grouping key consists only of entities and their properties, or it contains keys of all entities required to generate results on the client side. Either add '{column}' to the grouping key, remove complex elements of the grouping key, or rewrite the query to not use the 'GroupBy' operation. + + + Using 'Distinct' operation on a projection containing a collection is not supported. 'Reverse' could not be translated to the server because there is no ordering on the server side. diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index a19df13afcf..52f49308b52 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -387,11 +387,33 @@ private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType ent { Check.NotNull(source, nameof(source)); + var collectionShaperExpressionFinder = new CollectionShaperExpressionFinder(); + collectionShaperExpressionFinder.Visit(source.ShaperExpression); + if (collectionShaperExpressionFinder.Found) + { + throw new NotSupportedException(RelationalStrings.DistinctOnCollectionNotSupported); + } + ((SelectExpression)source.QueryExpression).ApplyDistinct(); return source; } + private sealed class CollectionShaperExpressionFinder : ExpressionVisitor + { + public bool Found { get; private set; } + + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is CollectionShaperExpression) + { + Found = true; + } + + return base.VisitExtension(extensionExpression); + } + } + /// protected override ShapedQueryExpression? TranslateElementAtOrDefault( ShapedQueryExpression source, diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index cbaffce4565..8101b64a1f8 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1119,6 +1119,20 @@ public IDictionary PushdownIntoSubquery() // if we can't propagate any identifier - clear them all instead // when adding collection join we detect this and throw appropriate exception _identifier.Clear(); + + if (IsDistinct + && projectionMap.Select(p => p.Value).All(x => x is ColumnExpression)) + { + // for distinct try to use entire projection as identifiers + _identifier.AddRange(projectionMap.Select(p => p.Value).Select(x => ((ColumnExpression)x, x.TypeMapping!.Comparer))); + } + else if (GroupBy.Count > 0 + && GroupBy.All(x => x is ColumnExpression)) + { + // for group by try to use grouping key as identifiers + _identifier.AddRange(GroupBy.Select(x => ((ColumnExpression)x, x.TypeMapping!.Comparer))); + } + break; } } @@ -1409,7 +1423,7 @@ public CollectionShaperExpression AddCollectionProjection( var parentIdentifier = GetIdentifierAccessor(identifierFromParent).Item1; innerSelectExpression.ApplyProjection(); - ValidateIdentifyingProjection(innerSelectExpression); + RemapIdentifiers(innerSelectExpression); for (var i = 0; i < identifierFromParent.Count; i++) { @@ -1511,7 +1525,7 @@ public CollectionShaperExpression AddCollectionProjection( var innerClientEval = innerSelectExpression.Projection.Count > 0; innerSelectExpression.ApplyProjection(); - ValidateIdentifyingProjection(innerSelectExpression); + RemapIdentifiers(innerSelectExpression); if (collectionIndex == 0) { @@ -1635,24 +1649,72 @@ public CollectionShaperExpression AddCollectionProjection( return result; } - static void ValidateIdentifyingProjection(SelectExpression selectExpression) + static void RemapIdentifiers(SelectExpression innerSelectExpression) { - if (selectExpression.IsDistinct - || selectExpression.GroupBy.Count > 0) + if (innerSelectExpression.IsDistinct + || innerSelectExpression.GroupBy.Count > 0) { - var innerSelectProjectionExpressions = selectExpression._projection.Select(p => p.Expression).ToList(); - foreach (var innerSelectIdentifier in selectExpression._identifier) + if (!IdentifiersInProjection(innerSelectExpression, out var missingIdentifier)) { - if (!innerSelectProjectionExpressions.Contains(innerSelectIdentifier.Column) - && (selectExpression.GroupBy.Count == 0 - || !selectExpression.GroupBy.Contains(innerSelectIdentifier.Column))) + if (innerSelectExpression.IsDistinct) { - throw new InvalidOperationException(RelationalStrings.MissingIdentifyingProjectionInDistinctGroupBySubquery( - innerSelectIdentifier.Column.Table.Alias + "." + innerSelectIdentifier.Column.Name)); + if (innerSelectExpression._projection.All(p => p.Expression is ColumnExpression)) + { + innerSelectExpression._identifier.Clear(); + foreach (var projection in innerSelectExpression._projection) + { + innerSelectExpression._identifier.Add(((ColumnExpression)projection.Expression, projection.Expression.TypeMapping!.Comparer)); + } + } + else + { + throw new InvalidOperationException( + RelationalStrings.UnableToTranslateSubqueryWithDistinct( + missingIdentifier.Table.Alias + "." + missingIdentifier.Name)); + } + } + else + { + if (innerSelectExpression.GroupBy.All(g => g is ColumnExpression)) + { + innerSelectExpression._identifier.Clear(); + foreach (var grouping in innerSelectExpression.GroupBy) + { + innerSelectExpression._identifier.Add(((ColumnExpression)grouping, grouping.TypeMapping!.Comparer)); + } + } + else + { + throw new InvalidOperationException( + RelationalStrings.UnableToTranslateSubqueryWithGroupBy( + missingIdentifier.Table.Alias + "." + missingIdentifier.Name)); + } } } } } + + static bool IdentifiersInProjection( + SelectExpression selectExpression, + [CA.NotNullWhen(false)] out ColumnExpression? missingIdentifier) + { + var innerSelectProjectionExpressions = selectExpression._projection.Select(p => p.Expression).ToList(); + foreach (var innerSelectIdentifier in selectExpression._identifier) + { + if (!innerSelectProjectionExpressions.Contains(innerSelectIdentifier.Column) + && (selectExpression.GroupBy.Count == 0 + || !selectExpression.GroupBy.Contains(innerSelectIdentifier.Column))) + { + missingIdentifier = innerSelectIdentifier.Column; + + return false; + } + } + + missingIdentifier = null; + + return true; + } } private sealed class EntityShaperNullableMarkingExpressionVisitor : ExpressionVisitor @@ -1709,12 +1771,54 @@ private static SqlExpression MakeNullable(SqlExpression sqlExpression) joinPredicate = RemoveRedundantNullChecks(joinPredicate, columnExpressions); } + // we can't convert apply to join in case of distinct and groupby, if the projection doesn't already contain the join keys + // since we can't add the missing keys to the projection - only convert to join if all the keys are already there + if (joinPredicate != null + && (selectExpression.IsDistinct + || selectExpression.GroupBy.Count > 0)) + { + // if select expression has only one table and projecting an entity + // we are guaranteed that the join key will be in the projection + // even if currently the projection is not populated + if (selectExpression.Tables.Count != 1 + || selectExpression._projectionMapping.Count != 1 + || selectExpression._projectionMapping.First().Value is not EntityProjectionExpression) + { + var innerKeyColumns = InnerKeyColumns(selectExpression.Tables, joinPredicate); + foreach (var innerColumn in innerKeyColumns) + { + if (!selectExpression.Projection.Select(p => p.Expression).Contains(innerColumn)) + { + return null; + } + } + } + } + selectExpression.Predicate = predicate; return joinPredicate; } return null; + + static List InnerKeyColumns(IEnumerable tables, SqlExpression joinPredicate) + { + if (joinPredicate is SqlBinaryExpression sqlBinaryExpression) + { + var leftColumns = InnerKeyColumns(tables, sqlBinaryExpression.Left); + var rightColumns = InnerKeyColumns(tables, sqlBinaryExpression.Right); + + return leftColumns.Concat(rightColumns).ToList(); + } + else if (joinPredicate is ColumnExpression columnExpression + && tables.Contains(columnExpression.Table)) + { + return new List { columnExpression }; + } + + return new List(); + } } private SqlExpression? TryExtractJoinKey( diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 0fe02295743..2338d77a1a1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -4222,6 +4222,24 @@ public override Task Using_string_Equals_with_StringComparison_throws_informativ CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison); } + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Select_nested_collection_with_distinct(bool async) + { + return base.Select_nested_collection_with_distinct(async); + } + + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns(bool async) + { + return base.Correlated_collection_with_distinct_without_default_identifiers_projecting_columns(async); + } + + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns_with_navigation(bool async) + { + return base.Correlated_collection_with_distinct_without_default_identifiers_projecting_columns_with_navigation(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs index 3808f490298..a0f96215a6e 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindSelectQueryCosmosTest.cs @@ -1143,12 +1143,34 @@ public override Task Projecting_multiple_collection_with_same_constant_works(boo return base.Projecting_multiple_collection_with_same_constant_works(async); } - [ConditionalTheory(Skip = "Issue#17246")] - public override async Task Projecting_after_navigation_and_distinct_throws(bool isAsync) + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Projecting_after_navigation_and_distinct(bool async) { - await base.Projecting_after_navigation_and_distinct_throws(isAsync); + return base.Projecting_after_navigation_and_distinct(async); + } - AssertSql(" "); + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Correlated_collection_after_distinct_with_complex_projection_containing_original_identifier(bool async) + { + return base.Correlated_collection_after_distinct_with_complex_projection_containing_original_identifier(async); + } + + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Correlated_collection_after_distinct_not_containing_original_identifier(bool async) + { + return base.Correlated_collection_after_distinct_not_containing_original_identifier(async); + } + + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(bool async) + { + return base.Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(async); + } + + [ConditionalTheory(Skip = "Cross collection join Issue#17246")] + public override Task Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(bool async) + { + return base.Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(async); } public override Task Reverse_without_explicit_ordering(bool async) diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs index bdcf03e0644..765acc50b26 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs @@ -65,6 +65,10 @@ public override void Client_code_using_instance_in_static_method() public override void Client_code_using_instance_method_throws() => base.Client_code_using_instance_method_throws(); + [ConditionalTheory(Skip = "Issue#24291")] + public override Task Select_nested_collection_with_distinct(bool async) + => base.Select_nested_collection_with_distinct(async); + public override async Task Max_on_empty_sequence_throws(bool async) { using var context = CreateContext(); diff --git a/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs index aaab523a3dd..9e629ed5e46 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/GearsOfWarQueryRelationalTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -21,37 +22,22 @@ protected GearsOfWarQueryRelationalTestBase(TFixture fixture) [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Correlated_collection_with_Distinct_missing_indentifying_columns_in_projection(bool async) + public override async Task Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) { var message = (await Assert.ThrowsAsync( - () => AssertQuery( - async, - ss => ss.Set() - .OrderBy(g => g.Nickname) - .Select(g => g.Weapons.SelectMany(x => x.Owner.AssignedCity.BornGears) - .Select(x => (bool?)x.HasSoulPatch).Distinct().ToList())))).Message; + () => base.Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async))).Message; - Assert.Equal(RelationalStrings.MissingIdentifyingProjectionInDistinctGroupBySubquery("w.Id"), message); + Assert.Equal(RelationalStrings.UnableToTranslateSubqueryWithGroupBy("w.Id"), message); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual async Task Correlated_collection_with_GroupBy_missing_indentifying_columns_in_projection(bool async) + public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column_also_projecting_complex_expressions(bool async) { var message = (await Assert.ThrowsAsync( - () => AssertQuery( - async, - ss => ss.Set() - .Select(m => new - { - m.Id, - grouping = m.ParticipatingSquads - .Select(ps => ps.SquadId) - .GroupBy(s => s) - .Select(g => new { g.Key, Count = g.Count() }) - })))).Message; - - Assert.Equal(RelationalStrings.MissingIdentifyingProjectionInDistinctGroupBySubquery("s.MissionId"), message); + () => base.Correlated_collection_with_distinct_not_projecting_identifier_column_also_projecting_complex_expressions(async))).Message; + + Assert.Equal(RelationalStrings.UnableToTranslateSubqueryWithDistinct("w.Id"), message); } public override async Task Client_eval_followed_by_aggregate_operation(bool async) @@ -140,6 +126,103 @@ await AssertQuery( elementSorter: e => e.Name); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Projecting_correlated_collection_followed_by_Distinct(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .Select(g => g.Weapons) + .Distinct(), + elementSorter: e => e.OrderBy(w => w.Id).FirstOrDefault().Id, + elementAsserter: (e, a) => AssertCollection(e, a)))).Message; + + Assert.Equal(RelationalStrings.DistinctOnCollectionNotSupported, message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Projecting_some_properties_as_well_as_correlated_collection_followed_by_Distinct(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .Select(g => new { g.FullName, g.HasSoulPatch, g.Weapons }) + .Distinct(), + elementSorter: e => e.FullName, + elementAsserter: (e, a) => + { + Assert.Equal(e.FullName, a.FullName); + Assert.Equal(e.HasSoulPatch, a.HasSoulPatch); + AssertCollection(e.Weapons, a.Weapons); + }))).Message; + + Assert.Equal(RelationalStrings.DistinctOnCollectionNotSupported, message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Projecting_entity_as_well_as_correlated_collection_followed_by_Distinct(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .Select(g => new { g, g.Weapons }) + .Distinct(), + elementSorter: e => e.g.FullName, + elementAsserter: (e, a) => + { + AssertEqual(e.g, a.g); + AssertCollection(e.Weapons, a.Weapons); + }))).Message; + + Assert.Equal(RelationalStrings.DistinctOnCollectionNotSupported, message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Projecting_entity_as_well_as_complex_correlated_collection_followed_by_Distinct(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .Select(g => new { g, Weapons = g.Weapons.Where(w => w.Id == g.SquadId).ToList() }) + .Distinct(), + elementSorter: e => e.g.FullName, + elementAsserter: (e, a) => + { + AssertEqual(e.g, a.g); + AssertCollection(e.Weapons, a.Weapons); + }))).Message; + + Assert.Equal(RelationalStrings.DistinctOnCollectionNotSupported, message); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Projecting_entity_as_well_as_correlated_collection_of_scalars_followed_by_Distinct(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .Select(g => new { g, Ids = g.Weapons.Select(w => w.Id).ToList() }) + .Distinct(), + elementSorter: e => e.g.FullName, + elementAsserter: (e, a) => + { + AssertEqual(e.g, a.g); + AssertCollection(e.Ids, a.Ids); + }))).Message; + + Assert.Equal(RelationalStrings.DistinctOnCollectionNotSupported, message); + } + protected virtual bool CanExecuteQueryString => false; diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs index fe9f724f3c0..f904d3c5224 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs @@ -19,33 +19,6 @@ protected NorthwindGroupByQueryRelationalTestBase(TFixture fixture) { } - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(bool async) - { - var message = (await Assert.ThrowsAsync( - () => AssertQuery( - async, - ss => ss.Set() - .Where(c => c.CustomerID.StartsWith("A")) - .Select(c => c.Customer.City) - .Distinct() - .Select(c => new - { - c1 = ss.Set().GroupBy(p => p.ProductID).Select(g => g.Key).ToArray(), - c2 = ss.Set().GroupBy(p => p.ProductID).Select(g => g.Count()).ToArray() - }), - assertOrder: true, - elementAsserter: (e, a) => - { - AssertCollection(e.c1, a.c1); - AssertCollection(e.c2, a.c2); - }))).Message; - - Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, message); - } - - [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Complex_query_with_groupBy_in_subquery4(bool async) @@ -70,28 +43,7 @@ public virtual async Task Complex_query_with_groupBy_in_subquery4(bool async) AssertCollection(e.Subquery, a.Subquery); }))).Message; - Assert.Equal(RelationalStrings.MissingIdentifyingProjectionInDistinctGroupBySubquery("o.OrderID"), message); - } - - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public virtual async Task Select_nested_collection_with_distinct(bool async) - { - var message = (await Assert.ThrowsAsync( - () => AssertQuery( - async, - ss => ss.Set() - .OrderBy(c => c.CustomerID) - .Where(c => c.CustomerID.StartsWith("A")) - .Select( - c => c.Orders.Any() - ? c.Orders.Select(o => o.CustomerID).Distinct().ToArray() - : Array.Empty()), - assertOrder: true, - elementAsserter: (e, a) => AssertCollection(e, a)))).Message; - - Assert.Equal(RelationalStrings.MissingIdentifyingProjectionInDistinctGroupBySubquery("o.OrderID"), message); + Assert.Equal(RelationalStrings.UnableToTranslateSubqueryWithGroupBy("o.OrderID"), message); } protected virtual bool CanExecuteQueryString diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs index 8c80ecabe34..9435226fea5 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSelectQueryRelationalTestBase.cs @@ -1,9 +1,13 @@ // 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; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; namespace Microsoft.EntityFrameworkCore.Query { @@ -15,6 +19,37 @@ protected NorthwindSelectQueryRelationalTestBase(TFixture fixture) { } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Correlated_collection_after_groupby_with_complex_projection_not_containing_original_identifier(bool async) + { + var filteredOrderIds = new[] { 10248, 10249, 10250 }; + + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => new { o.CustomerID, Complex = o.OrderDate.Value.Month }) + .Select(g => new { g.Key, Aggregate = g.Count() }) + .Select(c => new + { + c.Key.CustomerID, + c.Key.Complex, + Subquery = (from x in ss.Set() + where x.CustomerID == c.Key.CustomerID && filteredOrderIds.Contains(x.OrderID) + select new { Outer = c.Key.CustomerID, Inner = x.OrderID, x.OrderDate }).ToList() + }), + elementSorter: e => (e.CustomerID, e.Complex), + elementAsserter: (e, a) => + { + Assert.Equal(e.CustomerID, a.CustomerID); + Assert.Equal(e.Complex, a.Complex); + AssertCollection(e.Subquery, a.Subquery, elementSorter: ee => ee.Outer); + }))).Message; + + Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, message); + } + public override Task Select_bool_closure_with_order_by_property_with_cast_to_nullable(bool async) { return AssertTranslationFailed(() => base.Select_bool_closure_with_order_by_property_with_cast_to_nullable(async)); diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 07ac0e31c9a..2fb62b71053 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -8262,6 +8262,311 @@ public virtual Task Projecting_property_converted_to_nullable_and_use_it_in_orde assertOrder: true); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_distinct_projecting_identifier_column(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Id, w.Name }) + .Distinct().ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Id, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_distinct_projecting_identifier_column_and_correlation_key(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Id, w.Name, w.OwnerFullName }) + .Distinct().ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Id, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Name, aa.Name); + Assert.Equal(ee.OwnerFullName, aa.OwnerFullName); + }); + }); + } + + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_distinct_projecting_identifier_column_composite_key(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + s => new + { + Key = s.Id, + Subquery = s.Members + .Select(m => new { m.Nickname, m.SquadId, m.HasSoulPatch }) + .Distinct().ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Nickname, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Nickname, aa.Nickname); + Assert.Equal(ee.SquadId, aa.SquadId); + Assert.Equal(ee.HasSoulPatch, aa.HasSoulPatch); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_distinct_not_projecting_identifier_column(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Name, w.IsAutomatic }) + .Distinct().ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Name, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Name, aa.Name); + Assert.Equal(ee.IsAutomatic, aa.IsAutomatic); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_distinct_not_projecting_identifier_column_also_projecting_complex_expressions(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Name, w.IsAutomatic, w.OwnerFullName.Length }) + .Distinct().ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Name, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Name, aa.Name); + Assert.Equal(ee.IsAutomatic, aa.IsAutomatic); + Assert.Equal(ee.Length, aa.Length); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_groupby_not_projecting_identifier_column_but_only_grouping_key_in_final_projection(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Name, w.IsAutomatic }) + .GroupBy(x => x.IsAutomatic) + .Select(x => new { x.Key }).ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Key, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Key, aa.Key); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Name, w.IsAutomatic }) + .GroupBy(x => x.IsAutomatic) + .Select(x => new { x.Key, Count = x.Count() }).ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Key, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Key, aa.Key); + Assert.Equal(ee.Count, aa.Count); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection_multiple_grouping_keys(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Name, w.IsAutomatic }) + .GroupBy(x => new { x.IsAutomatic, x.Name }) + .Select(x => new { x.Key, Count = x.Count() }).ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Key.Name, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Key.Name, aa.Key.Name); + Assert.Equal(ee.Key.IsAutomatic, aa.Key.IsAutomatic); + Assert.Equal(ee.Count, aa.Count); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_groupby_with_complex_grouping_key_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + g => new + { + Key = g.Nickname, + Subquery = g.Weapons + .Select(w => new { w.Name, w.IsAutomatic }) + .GroupBy(x => x.Name.Length) + .Select(x => new { x.Key, Count = x.Count() }).ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.Key, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.Key, aa.Key); + Assert.Equal(ee.Count, aa.Count); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .OrderBy(g => g.Nickname) + .Select(g => g.Weapons.SelectMany(x => x.Owner.AssignedCity.BornGears) + .Select(x => (bool?)x.HasSoulPatch).Distinct().ToList()), + ss => ss.Set() + .OrderBy(g => g.Nickname) + .Select(g => g.Weapons.SelectMany(x => x.Owner.AssignedCity.Maybe(x => x.BornGears) ?? new List()) + .Select(x => (bool?)x.HasSoulPatch).Distinct().ToList()), + elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => ee), + assertOrder: true); + } + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index 1541aa62f75..4a22b06d5c9 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -3037,5 +3037,32 @@ public virtual Task AsEnumerable_in_subquery_for_GroupBy(bool async) } #endregion + + #region GroupByAndDistinctWithCorrelatedCollection + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Where(c => c.CustomerID.StartsWith("A")) + .Select(c => c.Customer.City) + .Distinct() + .Select(c => new + { + c1 = ss.Set().GroupBy(p => p.ProductID).Select(g => g.Key).ToArray(), + c2 = ss.Set().GroupBy(p => p.ProductID).Select(g => g.Count()).ToArray() + }), + assertOrder: true, + elementAsserter: (e, a) => + { + AssertCollection(e.c1, a.c1); + AssertCollection(e.c2, a.c2); + }); + } + + #endregion } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index 1c41d138e3b..5477dd4129e 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -6448,5 +6448,86 @@ public virtual async Task Load_should_track_results(bool async) Assert.Equal(91, context.ChangeTracker.Entries().Count()); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + c => new + { + Key = c.CustomerID, + Subquery = c.Orders + .Select(o => new { First = o.OrderID, Second = o.OrderDate }) + .Distinct().ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.First, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.First, aa.First); + Assert.Equal(ee.Second, aa.Second); + }); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns_with_navigation(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .Select( + c => new + { + Key = c.CustomerID, + Subquery = c.Orders + .Select(o => new { First = o.OrderID, Second = o.OrderDate, Third = o.Customer.City }) + .Distinct().ToList() + }), + elementSorter: e => e.Key, + elementAsserter: (e, a) => + { + Assert.Equal(e.Key, a.Key); + AssertCollection( + e.Subquery, + a.Subquery, + elementSorter: ee => ee.First, + elementAsserter: (ee, aa) => + { + Assert.Equal(ee.First, aa.First); + Assert.Equal(ee.Second, aa.Second); + Assert.Equal(ee.Third, aa.Third); + }); + }); + + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_nested_collection_with_distinct(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .OrderBy(c => c.CustomerID) + .Where(c => c.CustomerID.StartsWith("A")) + .Select( + c => c.Orders.Any() + ? c.Orders.Select(o => o.CustomerID).Distinct().ToArray() + : Array.Empty()), + assertOrder: true, + elementAsserter: (e, a) => AssertCollection(e, a)); + } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs index 0e29a6e7c01..fa2b9bfca65 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindSelectQueryTestBase.cs @@ -1795,7 +1795,7 @@ public virtual Task Projecting_multiple_collection_with_same_constant_works(bool [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task Projecting_after_navigation_and_distinct_throws(bool async) + public virtual Task Projecting_after_navigation_and_distinct(bool async) { var filteredOrderIds = new[] { 10248, 10249, 10250 }; @@ -1825,6 +1825,120 @@ public virtual Task Projecting_after_navigation_and_distinct_throws(bool async) }); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_after_distinct_with_complex_projection_containing_original_identifier(bool async) + { + var filteredOrderIds = new[] { 10248, 10249, 10250 }; + + return AssertQuery( + async, + ss => ss.Set() + .Select(o => new { o.OrderID, Complex = o.OrderDate.Value.Month }) + .Distinct() + .Select(c => new + { + c.OrderID, + c.Complex, + Subquery = (from x in ss.Set() + where x.OrderID == c.OrderID && filteredOrderIds.Contains(x.OrderID) + select new { Outer = c.OrderID, Inner = x.OrderID, x.OrderDate }).ToList() + }), + elementSorter: e => e.OrderID, + elementAsserter: (e, a) => + { + Assert.Equal(e.OrderID, a.OrderID); + Assert.Equal(e.Complex, a.Complex); + AssertCollection(e.Subquery, a.Subquery, elementSorter: ee => ee.Outer); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_after_distinct_not_containing_original_identifier(bool async) + { + var filteredOrderIds = new[] { 10248, 10249, 10250 }; + + return AssertQuery( + async, + ss => ss.Set() + .Select(o => new { o.OrderDate, o.CustomerID }) + .Distinct() + .Select(c => new + { + c.OrderDate, + c.CustomerID, + Subquery = (from x in ss.Set() + where x.CustomerID == c.CustomerID && filteredOrderIds.Contains(x.OrderID) + select new { Outer1 = c.OrderDate, Outer2 = c.CustomerID, Inner = x.OrderID, x.OrderDate }).ToList() + }), + elementSorter: e => (e.OrderDate, e.CustomerID), + elementAsserter: (e, a) => + { + Assert.Equal(e.OrderDate, a.OrderDate); + Assert.Equal(e.CustomerID, a.CustomerID); + AssertCollection(e.Subquery, a.Subquery, elementSorter: ee => (ee.Outer1, ee.Outer2, ee.Inner, ee.OrderDate)); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(bool async) + { + var filteredOrderIds = new[] { 10248, 10249, 10250 }; + + return AssertQuery( + async, + ss => ss.Set() + .Select(o => new { o.OrderDate, o.CustomerID, Complex = o.OrderDate.Value.Month }) + .Distinct() + .Select(c => new + { + c.OrderDate, + c.CustomerID, + c.Complex, + Subquery = (from x in ss.Set() + where x.CustomerID == c.CustomerID && filteredOrderIds.Contains(x.OrderID) + select new { Outer1 = c.OrderDate, Outer2 = c.CustomerID, Outer3 = c.Complex, Inner = x.OrderID, x.OrderDate }).ToList() + }), + elementSorter: e => (e.OrderDate, e.CustomerID, e.Complex), + elementAsserter: (e, a) => + { + Assert.Equal(e.OrderDate, a.OrderDate); + Assert.Equal(e.CustomerID, a.CustomerID); + Assert.Equal(e.Complex, a.Complex); + AssertCollection(e.Subquery, a.Subquery, elementSorter: ee => (ee.Outer1, ee.Outer2, ee.Outer3, ee.Inner, ee.OrderDate)); + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(bool async) + { + var filteredOrderIds = new[] { 10248, 10249, 10250 }; + + return AssertQuery( + async, + ss => ss.Set() + .GroupBy(o => new { o.OrderID, Complex = o.OrderDate.Value.Month }) + .Select(g => new { g.Key, Aggregate = g.Count() }) + .Select(c => new + { + c.Key.OrderID, + c.Key.Complex, + Subquery = (from x in ss.Set() + where x.OrderID == c.Key.OrderID && filteredOrderIds.Contains(x.OrderID) + select new { Outer = c.Key.OrderID, Inner = x.OrderID, x.OrderDate }).ToList() + }), + elementSorter: e => e.OrderID, + elementAsserter: (e, a) => + { + Assert.Equal(e.OrderID, a.OrderID); + Assert.Equal(e.Complex, a.Complex); + AssertCollection(e.Subquery, a.Subquery, elementSorter: ee => ee.Outer); + }); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Custom_projection_reference_navigation_PK_to_FK_optimization(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 70f2dd76309..db590d8e3eb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -281,10 +281,25 @@ WHERE [l2].[OneToMany_Required_Inverse3Id] IS NOT NULL AND [l2].[Level2_Required public override async Task SelectMany_with_navigation_and_Distinct(bool async) { - var message = (await Assert.ThrowsAsync( - () => base.SelectMany_with_navigation_and_Distinct(async))).Message; + await base.SelectMany_with_navigation_and_Distinct(async); - Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, message); + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [t].[Id], [t].[OneToOne_Required_PK_Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Level2_Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t0].[Id], [t0].[OneToOne_Required_PK_Date], [t0].[Level1_Optional_Id], [t0].[Level1_Required_Id], [t0].[Level2_Name], [t0].[OneToMany_Optional_Inverse2Id], [t0].[OneToMany_Required_Inverse2Id], [t0].[OneToOne_Optional_PK_Inverse2Id], [t0].[Id0] +FROM [Level1] AS [l] +CROSS APPLY ( + SELECT DISTINCT [l0].[Id], [l0].[OneToOne_Required_PK_Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Level2_Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id] + FROM [Level1] AS [l0] + INNER JOIN [Level1] AS [l1] ON [l0].[Id] = [l1].[Id] + WHERE ([l0].[OneToMany_Required_Inverse2Id] IS NOT NULL AND ([l0].[Level1_Required_Id] IS NOT NULL AND [l0].[OneToOne_Required_PK_Date] IS NOT NULL)) AND ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) +) AS [t] +LEFT JOIN ( + SELECT [l2].[Id], [l2].[OneToOne_Required_PK_Date], [l2].[Level1_Optional_Id], [l2].[Level1_Required_Id], [l2].[Level2_Name], [l2].[OneToMany_Optional_Inverse2Id], [l2].[OneToMany_Required_Inverse2Id], [l2].[OneToOne_Optional_PK_Inverse2Id], [l3].[Id] AS [Id0] + FROM [Level1] AS [l2] + INNER JOIN [Level1] AS [l3] ON [l2].[Id] = [l3].[Id] + WHERE [l2].[OneToMany_Required_Inverse2Id] IS NOT NULL AND ([l2].[Level1_Required_Id] IS NOT NULL AND [l2].[OneToOne_Required_PK_Date] IS NOT NULL) +) AS [t0] ON [l].[Id] = [t0].[OneToMany_Optional_Inverse2Id] +WHERE ([t].[OneToOne_Required_PK_Date] IS NOT NULL AND [t].[Level1_Required_Id] IS NOT NULL) AND [t].[OneToMany_Required_Inverse2Id] IS NOT NULL +ORDER BY [l].[Id], [t].[Id], [t].[OneToOne_Required_PK_Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Level2_Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t0].[Id], [t0].[Id0]"); } private void AssertSql(params string[] expected) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 870d4f333fb..07c34693993 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -7761,6 +7761,130 @@ ELSE NULL END, [t].[Note]"); } + public override async Task Correlated_collection_with_distinct_projecting_identifier_column(bool async) + { + await base.Correlated_collection_with_distinct_projecting_identifier_column(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[Id], [t].[Name] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT DISTINCT [w].[Id], [w].[Name] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] +) AS [t] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Id]"); + } + + public override async Task Correlated_collection_with_distinct_projecting_identifier_column_and_correlation_key(bool async) + { + await base.Correlated_collection_with_distinct_projecting_identifier_column_and_correlation_key(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[Id], [t].[Name], [t].[OwnerFullName] +FROM [Gears] AS [g] +LEFT JOIN ( + SELECT DISTINCT [w].[Id], [w].[Name], [w].[OwnerFullName] + FROM [Weapons] AS [w] +) AS [t] ON [g].[FullName] = [t].[OwnerFullName] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Id]"); + } + + public override async Task Correlated_collection_with_distinct_projecting_identifier_column_composite_key(bool async) + { + await base.Correlated_collection_with_distinct_projecting_identifier_column_composite_key(async); + + AssertSql( + @"SELECT [s].[Id], [t].[Nickname], [t].[SquadId], [t].[HasSoulPatch] +FROM [Squads] AS [s] +LEFT JOIN ( + SELECT DISTINCT [g].[Nickname], [g].[SquadId], [g].[HasSoulPatch] + FROM [Gears] AS [g] +) AS [t] ON [s].[Id] = [t].[SquadId] +ORDER BY [s].[Id], [t].[Nickname], [t].[SquadId]"); + } + + public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column(bool async) + { + await base.Correlated_collection_with_distinct_not_projecting_identifier_column(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[Name], [t].[IsAutomatic] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT DISTINCT [w].[Name], [w].[IsAutomatic] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] +) AS [t] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Name], [t].[IsAutomatic]"); + } + + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_but_only_grouping_key_in_final_projection(bool async) + { + await base.Correlated_collection_with_groupby_not_projecting_identifier_column_but_only_grouping_key_in_final_projection(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[Key] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT [w].[IsAutomatic] AS [Key] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] + GROUP BY [w].[IsAutomatic] +) AS [t] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Key]"); + } + + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + { + await base.Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[Key], [t].[Count] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT [w].[IsAutomatic] AS [Key], COUNT(*) AS [Count] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] + GROUP BY [w].[IsAutomatic] +) AS [t] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Key]"); + } + + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection_multiple_grouping_keys(bool async) + { + await base.Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection_multiple_grouping_keys(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[IsAutomatic], [t].[Name], [t].[Count] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT [w].[IsAutomatic], [w].[Name], COUNT(*) AS [Count] + FROM [Weapons] AS [w] + WHERE [g].[FullName] = [w].[OwnerFullName] + GROUP BY [w].[IsAutomatic], [w].[Name] +) AS [t] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[IsAutomatic], [t].[Name]"); + } + + public override async Task Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(bool async) + { + await base.Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[HasSoulPatch] +FROM [Gears] AS [g] +OUTER APPLY ( + SELECT DISTINCT [g1].[HasSoulPatch] + FROM [Weapons] AS [w] + LEFT JOIN [Gears] AS [g0] ON [w].[OwnerFullName] = [g0].[FullName] + LEFT JOIN [Cities] AS [c] ON [g0].[AssignedCityName] = [c].[Name] + INNER JOIN [Gears] AS [g1] ON [c].[Name] = [g1].[CityOfBirthName] + WHERE [g].[FullName] = [w].[OwnerFullName] +) AS [t] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[HasSoulPatch]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 43b97c24e9a..9d82150d1f7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -2508,6 +2508,31 @@ FROM [Orders] AS [o] GROUP BY [o].[CustomerID]"); } + public override async Task Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(bool async) + { + await base.Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(async); + + AssertSql( + @"SELECT [t].[City], [t0].[ProductID], [t1].[c], [t1].[ProductID] +FROM ( + SELECT DISTINCT [c].[City] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] + WHERE [o].[CustomerID] IS NOT NULL AND ([o].[CustomerID] LIKE N'A%') +) AS [t] +OUTER APPLY ( + SELECT [p].[ProductID] + FROM [Products] AS [p] + GROUP BY [p].[ProductID] +) AS [t0] +OUTER APPLY ( + SELECT COUNT(*) AS [c], [p0].[ProductID] + FROM [Products] AS [p0] + GROUP BY [p0].[ProductID] +) AS [t1] +ORDER BY [t].[City], [t0].[ProductID], [t1].[ProductID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index ac654220ab8..7a8bb4bec61 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -5392,6 +5392,58 @@ await context.Database.ExecuteSqlRawAsync( } } + public override async Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns(bool async) + { + await base.Correlated_collection_with_distinct_without_default_identifiers_projecting_columns(async); + + AssertSql( + @"SELECT [c].[CustomerID], [t].[First], [t].[Second] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT DISTINCT [o].[OrderID] AS [First], [o].[OrderDate] AS [Second] + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [t] +ORDER BY [c].[CustomerID], [t].[First]"); + } + + public override async Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns_with_navigation(bool async) + { + await base.Correlated_collection_with_distinct_without_default_identifiers_projecting_columns_with_navigation(async); + + AssertSql( + @"SELECT [c].[CustomerID], [t].[First], [t].[Second], [t].[Third] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT DISTINCT [o].[OrderID] AS [First], [o].[OrderDate] AS [Second], [c0].[City] AS [Third] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c0] ON [o].[CustomerID] = [c0].[CustomerID] + WHERE [c].[CustomerID] = [o].[CustomerID] +) AS [t] +ORDER BY [c].[CustomerID], [t].[First], [t].[Second], [t].[Third]"); + } + + public override async Task Select_nested_collection_with_distinct(bool async) + { + await base.Select_nested_collection_with_distinct(async); + + AssertSql( + @"SELECT CASE + WHEN EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[CustomerID] = [o].[CustomerID]) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [c].[CustomerID], [t].[CustomerID] +FROM [Customers] AS [c] +LEFT JOIN ( + SELECT DISTINCT [o0].[CustomerID] + FROM [Orders] AS [o0] +) AS [t] ON [c].[CustomerID] = [t].[CustomerID] +WHERE [c].[CustomerID] LIKE N'A%' +ORDER BY [c].[CustomerID], [t].[CustomerID]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs index 0e9cac66dfc..b2f658cb03a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSelectQuerySqlServerTest.cs @@ -1426,14 +1426,6 @@ FROM [Customers] AS [c] ORDER BY [c].[CustomerID], [o].[OrderID], [o0].[OrderID]"); } - public override async Task Projecting_after_navigation_and_distinct_throws(bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.Projecting_after_navigation_and_distinct_throws(async))).Message; - - Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, message); - } - public override async Task Custom_projection_reference_navigation_PK_to_FK_optimization(bool async) { await base.Custom_projection_reference_navigation_PK_to_FK_optimization(async); @@ -1587,6 +1579,98 @@ WHERE [o].[OrderID] < 10300 ORDER BY [o].[OrderID]"); } + public override async Task Projecting_after_navigation_and_distinct(bool async) + { + await base.Projecting_after_navigation_and_distinct(async); + + AssertSql( + @"SELECT [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [t0].[CustomerID], [t0].[OrderID], [t0].[OrderDate] +FROM ( + SELECT DISTINCT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] + FROM [Orders] AS [o] + LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +) AS [t] +OUTER APPLY ( + SELECT [t].[CustomerID], [o0].[OrderID], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] IN (10248, 10249, 10250) AND (([t].[CustomerID] = [o0].[CustomerID]) OR ([t].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) +) AS [t0] +ORDER BY [t].[CustomerID], [t].[Address], [t].[City], [t].[CompanyName], [t].[ContactName], [t].[ContactTitle], [t].[Country], [t].[Fax], [t].[Phone], [t].[PostalCode], [t].[Region], [t0].[OrderID]"); + } + + public override async Task Correlated_collection_after_distinct_with_complex_projection_containing_original_identifier(bool async) + { + await base.Correlated_collection_after_distinct_with_complex_projection_containing_original_identifier(async); + + AssertSql( + @"SELECT [t].[OrderID], [t].[Complex], [t0].[Outer], [t0].[Inner], [t0].[OrderDate] +FROM ( + SELECT DISTINCT [o].[OrderID], DATEPART(month, [o].[OrderDate]) AS [Complex] + FROM [Orders] AS [o] +) AS [t] +OUTER APPLY ( + SELECT [t].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] IN (10248, 10249, 10250) AND ([t].[OrderID] = [o0].[OrderID]) +) AS [t0] +ORDER BY [t].[OrderID], [t0].[Inner]"); + } + + public override async Task Correlated_collection_after_distinct_not_containing_original_identifier(bool async) + { + await base.Correlated_collection_after_distinct_not_containing_original_identifier(async); + + AssertSql( + @"SELECT [t].[OrderDate], [t].[CustomerID], [t0].[Outer1], [t0].[Outer2], [t0].[Inner], [t0].[OrderDate] +FROM ( + SELECT DISTINCT [o].[OrderDate], [o].[CustomerID] + FROM [Orders] AS [o] +) AS [t] +OUTER APPLY ( + SELECT [t].[OrderDate] AS [Outer1], [t].[CustomerID] AS [Outer2], [o0].[OrderID] AS [Inner], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] IN (10248, 10249, 10250) AND (([t].[CustomerID] = [o0].[CustomerID]) OR ([t].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) +) AS [t0] +ORDER BY [t].[OrderDate], [t].[CustomerID], [t0].[Inner]"); + } + + public override async Task Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(bool async) + { + await base.Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(async); + + AssertSql( + @"SELECT [t].[OrderDate], [t].[CustomerID], [t].[Complex], [t0].[Outer1], [t0].[Outer2], [t0].[Outer3], [t0].[Inner], [t0].[OrderDate] +FROM ( + SELECT DISTINCT [o].[OrderDate], [o].[CustomerID], DATEPART(month, [o].[OrderDate]) AS [Complex] + FROM [Orders] AS [o] +) AS [t] +OUTER APPLY ( + SELECT [t].[OrderDate] AS [Outer1], [t].[CustomerID] AS [Outer2], [t].[Complex] AS [Outer3], [o0].[OrderID] AS [Inner], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] IN (10248, 10249, 10250) AND (([t].[CustomerID] = [o0].[CustomerID]) OR ([t].[CustomerID] IS NULL AND [o0].[CustomerID] IS NULL)) +) AS [t0] +ORDER BY [t].[OrderDate], [t].[CustomerID], [t].[Complex], [t0].[Inner]"); + } + + public override async Task Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(bool async) + { + await base.Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(async); + + AssertSql( + @"SELECT [t].[OrderID], [t].[c], [t0].[Outer], [t0].[Inner], [t0].[OrderDate] +FROM ( + SELECT [o].[OrderID], DATEPART(month, [o].[OrderDate]) AS [c] + FROM [Orders] AS [o] + GROUP BY [o].[OrderID], DATEPART(month, [o].[OrderDate]) +) AS [t] +OUTER APPLY ( + SELECT [t].[OrderID] AS [Outer], [o0].[OrderID] AS [Inner], [o0].[OrderDate] + FROM [Orders] AS [o0] + WHERE [o0].[OrderID] IN (10248, 10249, 10250) AND ([t].[OrderID] = [o0].[OrderID]) +) AS [t0] +ORDER BY [t].[OrderID], [t0].[Inner]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs index 715f67baf6a..2e3e158a2a5 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqliteTest.cs @@ -19,12 +19,10 @@ public ComplexNavigationsSharedTypeQuerySqliteTest(ComplexNavigationsSharedTypeQ } public override async Task SelectMany_with_navigation_and_Distinct(bool async) - { - var message = (await Assert.ThrowsAsync( - () => base.SelectMany_with_navigation_and_Distinct(async))).Message; - - Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, message); - } + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.SelectMany_with_navigation_and_Distinct(async))).Message); public override async Task Filtered_include_after_different_filtered_include_different_level(bool async) => Assert.Equal( diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs index aed1d9c5c6a..0e69da865c8 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs @@ -136,6 +136,42 @@ public override async Task Correlated_collection_with_inner_collection_reference (await Assert.ThrowsAsync( () => base.Correlated_collection_with_inner_collection_references_element_two_levels_up(async))).Message); + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async))).Message); + + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection_multiple_grouping_keys(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection_multiple_grouping_keys(async))).Message); + + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_but_only_grouping_key_in_final_projection(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_not_projecting_identifier_column_but_only_grouping_key_in_final_projection(async))).Message); + + public override async Task Correlated_collection_with_distinct_projecting_identifier_column(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_distinct_projecting_identifier_column(async))).Message); + + public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_distinct_not_projecting_identifier_column(async))).Message); + + public override async Task Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(async))).Message); + public override async Task Negate_on_binary_expression(bool async) { await base.Negate_on_binary_expression(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs index a8e5c490d60..4d88649e31a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs @@ -31,5 +31,11 @@ public override async Task Select_uncorrelated_collection_with_groupby_works(boo SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync( () => base.Select_uncorrelated_collection_with_groupby_works(async))).Message); + + public override async Task Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Select_uncorrelated_collection_with_groupby_when_outer_is_distinct(async))).Message); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs index 29da8fc7242..bc2aaac9ec3 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindMiscellaneousQuerySqliteTest.cs @@ -319,6 +319,18 @@ public override async Task Select_correlated_subquery_ordered(bool async) (await Assert.ThrowsAsync( () => base.Select_correlated_subquery_ordered(async))).Message); + public override async Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns_with_navigation(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_distinct_without_default_identifiers_projecting_columns_with_navigation(async))).Message); + + public override async Task Correlated_collection_with_distinct_without_default_identifiers_projecting_columns(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_distinct_without_default_identifiers_projecting_columns(async))).Message); + [ConditionalFact] public async Task Single_Predicate_Cancellation() { diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs index 6809096e0c7..bfd5f728e02 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSelectQuerySqliteTest.cs @@ -182,11 +182,11 @@ public override async Task SelectMany_whose_selector_references_outer_source(boo (await Assert.ThrowsAsync( () => base.SelectMany_whose_selector_references_outer_source(async))).Message); - public override async Task Projecting_after_navigation_and_distinct_throws(bool async) + public override async Task Projecting_after_navigation_and_distinct(bool async) => Assert.Equal( - RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, + SqliteStrings.ApplyNotSupported, (await Assert.ThrowsAsync( - () => base.Projecting_after_navigation_and_distinct_throws(async))).Message); + () => base.Projecting_after_navigation_and_distinct(async))).Message); public override async Task Select_nested_collection_deep(bool async) => Assert.Equal( @@ -194,6 +194,30 @@ public override async Task Select_nested_collection_deep(bool async) (await Assert.ThrowsAsync( () => base.Select_nested_collection_deep(async))).Message); + public override async Task Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_after_groupby_with_complex_projection_containing_original_identifier(async))).Message); + + public override async Task Correlated_collection_after_distinct_not_containing_original_identifier(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_after_distinct_not_containing_original_identifier(async))).Message); + + public override async Task Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_after_distinct_with_complex_projection_not_containing_original_identifier(async))).Message); + + public override async Task Correlated_collection_after_distinct_with_complex_projection_containing_original_identifier(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_after_distinct_with_complex_projection_containing_original_identifier(async))).Message); + [ConditionalTheory(Skip = "Issue#17324")] public override Task Project_single_element_from_collection_with_OrderBy_over_navigation_Take_and_FirstOrDefault_2(bool async) { diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs index e242dd9ecb6..056defc0748 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/TPTGearsOfWarQuerySqliteTest.cs @@ -137,6 +137,42 @@ public override async Task Correlated_collection_with_inner_collection_reference (await Assert.ThrowsAsync( () => base.Correlated_collection_with_inner_collection_references_element_two_levels_up(async))).Message); + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection(async))).Message); + + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection_multiple_grouping_keys(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_not_projecting_identifier_column_with_group_aggregate_in_final_projection_multiple_grouping_keys(async))).Message); + + public override async Task Correlated_collection_with_groupby_not_projecting_identifier_column_but_only_grouping_key_in_final_projection(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_groupby_not_projecting_identifier_column_but_only_grouping_key_in_final_projection(async))).Message); + + public override async Task Correlated_collection_with_distinct_projecting_identifier_column(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_distinct_projecting_identifier_column(async))).Message); + + public override async Task Correlated_collection_with_distinct_not_projecting_identifier_column(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_with_distinct_not_projecting_identifier_column(async))).Message); + + public override async Task Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(bool async) + => Assert.Equal( + SqliteStrings.ApplyNotSupported, + (await Assert.ThrowsAsync( + () => base.Correlated_collection_via_SelectMany_with_Distinct_missing_indentifying_columns_in_projection(async))).Message); + public override async Task Negate_on_binary_expression(bool async) { await base.Negate_on_binary_expression(async);