diff --git a/.gitignore b/.gitignore index 345de448057..69367c59bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -301,6 +301,9 @@ paket-files/ .idea/ *.sln.iml +# Visual Studio Code +.vscode/ + # CodeRush personal settings .cr/personal diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs index d08cbbb4c4f..f199ebc0d41 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -380,7 +380,7 @@ SqlExpression Process(Expression expression) jsonQuery.JsonColumn.TypeMapping, jsonQuery.IsNullable), - // As above, but for a complex JSON collectio + // As above, but for a complex JSON collection CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery } => new JsonScalarExpression( jsonQuery.JsonColumn, diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index aca5c888d99..51e06e5ed57 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -2086,7 +2086,7 @@ private void ApplySetOperation( var outerIdentifiers = select1._identifier.Count == select2._identifier.Count ? new ColumnExpression?[select1._identifier.Count] - : Array.Empty(); + : []; var entityProjectionIdentifiers = new List(); var entityProjectionValueComparers = new List(); var otherExpressions = new List<(SqlExpression Expression, ValueComparer Comparer)>(); @@ -2333,22 +2333,91 @@ StructuralTypeProjectionExpression ProcessStructuralType( var complexPropertyShaper1 = structuralProjection1.BindComplexProperty(complexProperty); var complexPropertyShaper2 = structuralProjection2.BindComplexProperty(complexProperty); - if (complexPropertyShaper1 is not StructuralTypeShaperExpression nonCollectionShaper1 - || complexPropertyShaper2 is not StructuralTypeShaperExpression nonCollectionShaper2) + switch ((complexPropertyShaper1, complexPropertyShaper2)) { - throw new NotImplementedException("Set operation over collection complex properties"); + // Set operation over type that contains a structural type mapped to table splitting - + // recurse to continue processing all the properties of the structural type + case + ( + StructuralTypeShaperExpression { ValueBufferExpression: StructuralTypeProjectionExpression projection1 }, + StructuralTypeShaperExpression { ValueBufferExpression: StructuralTypeProjectionExpression projection2 } + ): + var resultComplexProjection = ProcessStructuralType(projection1, projection2); + + var outerShaper = new RelationalStructuralTypeShaperExpression( + complexProperty.ComplexType, + resultComplexProjection, + resultComplexProjection.IsNullable); + + complexPropertyCache[complexProperty] = outerShaper; + break; + + // Set operation over type that contains a JSON-mapped complex type (non-collection) + case + ( + StructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression jsonQuery1 } shaper1, + StructuralTypeShaperExpression { ValueBufferExpression: JsonQueryExpression jsonQuery2 } shaper2 + ): + ProcessJson(jsonQuery1, jsonQuery2); + continue; + + // Set operation over type that contains a JSON-mapped complex type (collection) + case + ( + CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery1 } collection1, + CollectionResultExpression { QueryExpression: JsonQueryExpression jsonQuery2 } collection2 + ): + ProcessJson(jsonQuery1, jsonQuery2); + continue; + + default: + throw new UnreachableException(); } - var resultComplexProjection = ProcessStructuralType( - (StructuralTypeProjectionExpression)nonCollectionShaper1.ValueBufferExpression, - (StructuralTypeProjectionExpression)nonCollectionShaper2.ValueBufferExpression); + void ProcessJson(JsonQueryExpression jsonQuery1, JsonQueryExpression jsonQuery2) + { + Check.DebugAssert(jsonQuery1.StructuralType == jsonQuery2.StructuralType); + Check.DebugAssert(jsonQuery1.Type == jsonQuery2.Type); + + // Convert the JsonQueryExpression to a JsonScalarExpression, which is our current representation for a complex + // JSON in the SQL tree (as opposed to in the shaper) - see #36392. + var jsonScalar1 = new JsonScalarExpression( + jsonQuery1.JsonColumn, + jsonQuery1.Path, + jsonQuery1.Type, + jsonQuery1.JsonColumn.TypeMapping, + jsonQuery1.IsNullable); + var jsonScalar2 = new JsonScalarExpression( + jsonQuery2.JsonColumn, + jsonQuery2.Path, + jsonQuery2.Type, + jsonQuery2.JsonColumn.TypeMapping, + jsonQuery2.IsNullable); + + var alias = GenerateUniqueColumnAlias(complexProperty.Name.ToString()); + var innerProjection = new ProjectionExpression(jsonScalar1, alias); + select1._projection.Add(innerProjection); + select2._projection.Add(new ProjectionExpression(jsonScalar2, alias)); + + var outerColumn = CreateColumnExpression(innerProjection, setOperationAlias); + if (jsonScalar1.IsNullable || jsonScalar2.IsNullable) + { + outerColumn = outerColumn.MakeNullable(); + } + + var outerJsonQuery = new JsonQueryExpression( + jsonQuery1.StructuralType, + outerColumn, + keyPropertyMap: null, // For owned entities only, here we're processing a complex type + jsonQuery1.Path, + jsonQuery1.Type, + collection: jsonQuery1.IsCollection, + jsonQuery1.IsNullable || jsonQuery2.IsNullable); - var resultComplexShaper = new RelationalStructuralTypeShaperExpression( - complexProperty.ComplexType, - resultComplexProjection, - resultComplexProjection.IsNullable); + var outerShaper = new CollectionResultExpression(outerJsonQuery, complexProperty, complexProperty.ComplexType.ClrType); - complexPropertyCache[complexProperty] = resultComplexShaper; + complexPropertyCache[complexProperty] = outerShaper; + } } Check.DebugAssert( @@ -4167,10 +4236,11 @@ private static ColumnExpression CreateColumnExpression(ProjectionExpression subq column: subqueryProjection.Expression is ColumnExpression { Column: IColumnBase column } ? column : null, subqueryProjection.Type, subqueryProjection.Expression.TypeMapping!, - subqueryProjection.Expression switch + nullable: subqueryProjection.Expression switch { ColumnExpression c => c.IsNullable, SqlConstantExpression c => c.Value is null, + JsonScalarExpression j => j.IsNullable, _ => true }); diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs index 972e6460e6f..275452fc583 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs @@ -375,13 +375,10 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr var jsonColumn = selectExpression.CreateColumnExpression( jsonEachExpression, JsonEachValueColumnName, typeof(string), _typeMappingSource.FindMapping(typeof(string))); // TODO: nullable? - var containerColumnName = structuralType.GetContainerColumnName(); - Check.DebugAssert(containerColumnName is not null, "JsonQueryExpression to entity type without a container column name"); - // First step: build a SelectExpression that will execute json_each and project all properties and navigations out, e.g. // (SELECT value ->> 'a' AS a, value ->> 'b' AS b FROM json_each(c."JsonColumn", '$.Something.SomeCollection') - // We're only interested in properties which actually exist in the JSON, filter out uninteresting shadow keys + // We're only interested in properties which actually exist in the JSON, filter out uninteresting synthetic keys foreach (var property in structuralType.GetPropertiesInHierarchy()) { if (property.GetJsonPropertyName() is string jsonPropertyName) @@ -393,14 +390,14 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr propertyJsonScalarExpression[projectionMember] = new JsonScalarExpression( jsonColumn, - new[] { new PathSegment(property.GetJsonPropertyName()!) }, + [new PathSegment(property.GetJsonPropertyName()!)], property.ClrType.UnwrapNullableType(), property.GetRelationalTypeMapping(), property.IsNullable); } } - if (jsonQueryExpression.StructuralType is IEntityType entityType) + if (structuralType is IEntityType entityType) { foreach (var navigation in entityType.GetNavigationsInHierarchy() .Where( @@ -422,7 +419,20 @@ [new PathSegment(jsonNavigationName)], } } - // TODO: also add JsonScalarExpressions for complex properties, not just owned entities. #36296. + foreach (var complexProperty in structuralType.GetComplexProperties()) + { + var jsonNavigationName = complexProperty.ComplexType.GetJsonPropertyName(); + Check.DebugAssert(jsonNavigationName is not null, "Invalid complex property found on JSON-mapped structural type"); + + var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonNavigationName)); + + propertyJsonScalarExpression[projectionMember] = new JsonScalarExpression( + jsonColumn, + [new PathSegment(jsonNavigationName)], + typeof(string), + textTypeMapping, + jsonQueryExpression.IsNullable || complexProperty.IsNullable); + } selectExpression.ReplaceProjection(propertyJsonScalarExpression); diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index 80dceffb0a7..f8c62d0d5d5 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -49,7 +49,7 @@ protected override Expression VisitExtension(Expression expression) protected override Expression VisitMember(MemberExpression memberExpression) { var innerExpression = Visit(memberExpression.Expression); - return TryExpandNavigation(innerExpression, MemberIdentity.Create(memberExpression.Member)) + return TryExpandRelationship(innerExpression, MemberIdentity.Create(memberExpression.Member)) ?? memberExpression.Update(innerExpression); } @@ -58,21 +58,21 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp if (methodCallExpression.TryGetEFPropertyArguments(out var source, out var navigationName)) { source = Visit(source); - return TryExpandNavigation(source, MemberIdentity.Create(navigationName)) + return TryExpandRelationship(source, MemberIdentity.Create(navigationName)) ?? methodCallExpression.Update(null, new[] { source, methodCallExpression.Arguments[1] }); } if (methodCallExpression.TryGetIndexerArguments(Model, out source, out navigationName)) { source = Visit(source); - return TryExpandNavigation(source, MemberIdentity.Create(navigationName)) + return TryExpandRelationship(source, MemberIdentity.Create(navigationName)) ?? methodCallExpression.Update(source, new[] { methodCallExpression.Arguments[0] }); } return base.VisitMethodCall(methodCallExpression); } - private Expression? TryExpandNavigation(Expression? root, MemberIdentity memberIdentity) + private Expression? TryExpandRelationship(Expression? root, MemberIdentity memberIdentity) { if (root == null) { @@ -80,7 +80,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } var innerExpression = root.UnwrapTypeConversion(out var convertedType); - var entityReference = UnwrapEntityReference(innerExpression); + var structuralTypeReference = UnwrapStructuralTypeReference(innerExpression); + var entityReference = structuralTypeReference as EntityReference; if (entityReference is not null) { var entityType = entityReference.EntityType; @@ -117,8 +118,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var structuralType = entityReference is not null ? (ITypeBase)entityReference.EntityType - : innerExpression is ComplexPropertyReference complexReference - ? complexReference.Property.ComplexType + : structuralTypeReference is ComplexTypeReference complexTypeReference + ? complexTypeReference.ComplexType : null; if (structuralType is not null) @@ -1101,6 +1102,9 @@ public IReadOnlyDictionary ClonedNodesMa case EntityReference entityReference: return entityReference.Snapshot(); + case ComplexTypeReference complexTypeReference: + return complexTypeReference; + case NavigationTreeExpression navigationTreeExpression: if (!_clonedMap.TryGetValue(navigationTreeExpression, out var clonedNavigationTreeExpression)) { diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index fa2c54bec87..ed75f154261 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -485,7 +485,8 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) /// /// Queryable properties are not expanded (similar to . /// - private sealed class ComplexPropertyReference(Expression parent, IComplexProperty property) : Expression, IPrintableExpression + private sealed class ComplexPropertyReference(Expression parent, IComplexProperty complexProperty) + : Expression, IPrintableExpression { protected override Expression VisitChildren(ExpressionVisitor visitor) { @@ -495,7 +496,8 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) } public Expression Parent { get; private set; } = parent; - public new IComplexProperty Property { get; } = property; + public new IComplexProperty Property { get; } = complexProperty; + public ComplexTypeReference ComplexTypeReference { get; } = new(complexProperty.ComplexType); public override Type Type => Property.ClrType; @@ -516,6 +518,23 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) } } + private sealed class ComplexTypeReference(IComplexType complexType) : Expression, IPrintableExpression + { + public IComplexType ComplexType { get; } = complexType; + + public override ExpressionType NodeType + => ExpressionType.Extension; + + public override Type Type + => ComplexType.ClrType; + + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Append($"{nameof(ComplexTypeReference)}: {ComplexType.DisplayName()}"); + } + /// /// Queryable properties are not expanded (similar to . /// diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 45acce88fcd..bdef62ec943 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -251,6 +251,7 @@ protected override Expression VisitExtension(Expression extensionExpression) case NavigationExpansionExpression: case OwnedNavigationReference: + case ComplexPropertyReference: return extensionExpression; default: @@ -268,6 +269,13 @@ protected override Expression VisitMember(MemberExpression memberExpression) { var innerExpression = Visit(memberExpression.Expression); + // Handler access of a complex collection property over a complex non-collection property + if (memberExpression.Expression is ComplexPropertyReference { Property: { IsCollection: false } complexProperty } complexPropertyReference + && complexProperty.ComplexType.FindComplexProperty(memberExpression.Member) is IComplexProperty nestedComplexProperty) + { + return new ComplexPropertyReference(complexPropertyReference, nestedComplexProperty); + } + // Convert ICollection.Count to Count() if (memberExpression.Expression != null && innerExpression != null @@ -2079,9 +2087,9 @@ private Expression UnwrapCollectionMaterialization(Expression expression) GetParameterName("o")); } - case ComplexPropertyReference complexCollectionReference: + case ComplexPropertyReference { Property.IsCollection: true } complexCollectionReference: { - var currentTree = new NavigationTreeExpression(Expression.Default(complexCollectionReference.Type.GetSequenceType())); + var currentTree = new NavigationTreeExpression(complexCollectionReference.ComplexTypeReference); return new NavigationExpansionExpression( Expression.Call( @@ -2267,6 +2275,9 @@ private static Expression SnapshotExpression(Expression selector) case EntityReference entityReference: return entityReference.Snapshot(); + case ComplexTypeReference complexTypeReference: + return complexTypeReference; + case NavigationTreeExpression navigationTreeExpression: return SnapshotExpression(navigationTreeExpression.Value); @@ -2294,13 +2305,17 @@ private static Expression SnapshotExpression(Expression selector) } private static EntityReference? UnwrapEntityReference(Expression? expression) + => UnwrapStructuralTypeReference(expression) as EntityReference; + + private static Expression? UnwrapStructuralTypeReference(Expression? expression) => expression switch { EntityReference entityReference => entityReference, - NavigationTreeExpression navigationTreeExpression => UnwrapEntityReference(navigationTreeExpression.Value), + ComplexTypeReference complexTypeReference => complexTypeReference, + NavigationTreeExpression navigationTreeExpression => UnwrapStructuralTypeReference(navigationTreeExpression.Value), NavigationExpansionExpression navigationExpansionExpression when navigationExpansionExpression.CardinalityReducingGenericMethodInfo is not null - => UnwrapEntityReference(navigationExpansionExpression.PendingSelector), + => UnwrapStructuralTypeReference(navigationExpansionExpression.PendingSelector), OwnedNavigationReference ownedNavigationReference => ownedNavigationReference.EntityReference, _ => null, diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs index b06258474e0..5e9339aa3e7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionCosmosTest.cs @@ -108,6 +108,21 @@ FROM root c """); } + public override async Task Select_within_Select_within_Select_with_aggregates() + { + await base.Select_within_Select_within_Select_with_aggregates(); + + AssertSql( + """ +SELECT VALUE ( + SELECT VALUE SUM(( + SELECT VALUE MAX(n["Int"]) + FROM n IN r["NestedCollection"])) + FROM r IN c["RelatedCollection"]) +FROM root c +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs index 0e464f41b2e..aef5c28ad88 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit.Sdk; - namespace Microsoft.EntityFrameworkCore.Query.Relationships.OwnedNavigations; public class OwnedNavigationsProjectionCosmosTest : OwnedNavigationsProjectionTestBase diff --git a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs index 2350e82ea46..7b9bef673e2 100644 --- a/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs @@ -36,19 +36,23 @@ public class InMemoryComplianceTest : ComplianceTestBase typeof(RelationshipsCollectionTestBase<>), typeof(RelationshipsMiscellaneousTestBase<>), typeof(RelationshipsStructuralEqualityTestBase<>), + typeof(RelationshipsSetOperationsTestBase<>), typeof(NavigationsIncludeTestBase<>), typeof(NavigationsProjectionTestBase<>), typeof(NavigationsCollectionTestBase<>), typeof(NavigationsMiscellaneousTestBase<>), typeof(NavigationsStructuralEqualityTestBase<>), + typeof(NavigationsSetOperationsTestBase<>), typeof(OwnedNavigationsProjectionTestBase<>), typeof(OwnedNavigationsCollectionTestBase<>), typeof(OwnedNavigationsMiscellaneousTestBase<>), typeof(OwnedNavigationsStructuralEqualityTestBase<>), + typeof(OwnedNavigationsSetOperationsTestBase<>), typeof(ComplexPropertiesProjectionTestBase<>), typeof(ComplexPropertiesCollectionTestBase<>), typeof(ComplexPropertiesMiscellaneousTestBase<>), - typeof(ComplexPropertiesStructuralEqualityTestBase<>) + typeof(ComplexPropertiesStructuralEqualityTestBase<>), + typeof(ComplexPropertiesSetOperationsTestBase<>) }; protected override Assembly TargetAssembly { get; } = typeof(InMemoryComplianceTest).Assembly; diff --git a/test/EFCore.Relational.Specification.Tests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsRelationalTestBase.cs new file mode 100644 index 00000000000..73a2a2e47cb --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsRelationalTestBase.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Query.Relationships.ComplexProperties; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.ComplexJson; + +public abstract class ComplexJsonSetOperationsRelationalTestBase : ComplexPropertiesSetOperationsTestBase + where TFixture : ComplexJsonRelationalFixtureBase, new() +{ + public ComplexJsonSetOperationsRelationalTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + { + // #33485, #34849 (fails in the same way with regular navigations, not just complex JSON) + var exception = await Assert.ThrowsAsync( + () => base.On_related_projected(queryTrackingBehavior)); + + Assert.Equal( + RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, + exception.Message); + } + + + public override async Task Over_different_collection_properties() + { + // In complex type mapping, different properties are modeled as different structural types even if they share the same CLR type. + // As a result, their model definitions might differ (e.g. shadow properties) and we don't currently support set operations over them. + var exception = await Assert.ThrowsAsync(base.Over_different_collection_properties); + + Assert.Equal( + RelationalStrings.SetOperationOverDifferentStructuralTypes("RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType", "RootEntity.OptionalRelated#RelatedType.NestedCollection#NestedType"), + exception.Message); + } + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/Relationships/Navigations/NavigationsSetOperationsRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/Relationships/Navigations/NavigationsSetOperationsRelationalTestBase.cs new file mode 100644 index 00000000000..4b106ca75f2 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/Relationships/Navigations/NavigationsSetOperationsRelationalTestBase.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Navigations; + +public abstract class NavigationsSetOperationsRelationalTestBase : NavigationsSetOperationsTestBase + where TFixture : NavigationsRelationalFixtureBase, new() +{ + public NavigationsSetOperationsRelationalTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + { + // #33485, #34849 + var exception = await Assert.ThrowsAsync( + () => base.On_related_projected(queryTrackingBehavior)); + + Assert.Equal( + RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, + exception.Message); + } + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/Relationships/OwnedJson/OwnedJsonSetOperationsRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/Relationships/OwnedJson/OwnedJsonSetOperationsRelationalTestBase.cs new file mode 100644 index 00000000000..eef4e0cee86 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/Relationships/OwnedJson/OwnedJsonSetOperationsRelationalTestBase.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// All set operation tests fail over owned JSON collections: +// System.Collections.Generic.KeyNotFoundException : The given key 'Property: RootEntity.RelatedCollection#RelatedType.__synthesizedOrdinal (no field, int) Shadow Required PK AfterSave:Throw ValueGenerated.OnAdd' was not present in the dictionary. +// at System.Collections.Generic.Dictionary`2.get_Item(TKey key) +// at Microsoft.EntityFrameworkCore.Query.StructuralTypeProjectionExpression.BindProperty(IProperty property) in /Users/roji/projects/efcore/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs:line 365 diff --git a/test/EFCore.Relational.Specification.Tests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsRelationalTestBase.cs new file mode 100644 index 00000000000..fa19e2990d2 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsRelationalTestBase.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.OwnedNavigations; + +public abstract class OwnedNavigationsSetOperationsRelationalTestBase : OwnedNavigationsSetOperationsTestBase + where TFixture : OwnedNavigationsRelationalFixtureBase, new() +{ + public OwnedNavigationsSetOperationsRelationalTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + { + // #33485, #34849 + var exception = await Assert.ThrowsAsync( + () => base.On_related_projected(queryTrackingBehavior)); + + Assert.Equal( + RelationalStrings.InsufficientInformationToIdentifyElementOfCollectionJoin, + exception.Message); + } + + public override async Task Over_different_collection_properties() + { + // In owned navigation, different properties are modeled as different structural types even if they share the same CLR type. + // As a result, their model definitions might differ (e.g. shadow properties) and we don't currently support set operations over them. + var exception = await Assert.ThrowsAsync(base.Over_different_collection_properties); + + Assert.Equal( + RelationalStrings.SetOperationOverDifferentStructuralTypes("RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType", "RootEntity.OptionalRelated#RelatedType.NestedCollection#NestedType"), + exception.Message); + } + + public void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Specification.Tests/Query/Relationships/ComplexProperties/ComplexPropertiesSetOperationsTestBase.cs b/test/EFCore.Specification.Tests/Query/Relationships/ComplexProperties/ComplexPropertiesSetOperationsTestBase.cs new file mode 100644 index 00000000000..dd94c7a10e3 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/Relationships/ComplexProperties/ComplexPropertiesSetOperationsTestBase.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.ComplexProperties; + +public abstract class ComplexPropertiesSetOperationsTestBase(TFixture fixture) + : RelationshipsSetOperationsTestBase(fixture) + where TFixture : ComplexPropertiesFixtureBase, new() +{ + // TODO: the following is temporary until change tracking is implemented for complex JSON types (#35962) + private readonly TrackingRewriter _trackingRewriter = new(QueryTrackingBehavior.NoTracking); + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + { + var rewritten = _trackingRewriter.Visit(serverQueryExpression); + + return rewritten; + } +} diff --git a/test/EFCore.Specification.Tests/Query/Relationships/Navigations/NavigationsSetOperationsTestBase.cs b/test/EFCore.Specification.Tests/Query/Relationships/Navigations/NavigationsSetOperationsTestBase.cs new file mode 100644 index 00000000000..9d6ed07b7b7 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/Relationships/Navigations/NavigationsSetOperationsTestBase.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Navigations; + +public abstract class NavigationsSetOperationsTestBase(TFixture fixture) + : RelationshipsSetOperationsTestBase(fixture) + where TFixture : NavigationsFixtureBase, new() +{ +} diff --git a/test/EFCore.Specification.Tests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsTestBase.cs b/test/EFCore.Specification.Tests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsTestBase.cs new file mode 100644 index 00000000000..4b9e0bfc752 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsTestBase.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.OwnedNavigations; + +public abstract class OwnedNavigationsSetOperationsTestBase(TFixture fixture) + : RelationshipsSetOperationsTestBase(fixture) + where TFixture : OwnedNavigationsFixtureBase, new() +{ +} diff --git a/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsCollectionTestBase.cs b/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsCollectionTestBase.cs index 5f279a9f6c7..59aad4ad0f7 100644 --- a/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsCollectionTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsCollectionTestBase.cs @@ -63,6 +63,12 @@ public virtual Task Index_out_of_bounds() #endregion Index + [ConditionalFact] + public virtual Task Select_within_Select_within_Select_with_aggregates() + => AssertQuery( + ss => ss.Set().Select(e => + e.RelatedCollection.Select(r => r.NestedCollection.Select(n => n.Int).Max()).Sum())); + /// /// Utility for tests that depend on the collection being naturally ordered /// (e.g. JSON collection as opposed to a classical relational collection navigation, which is unordered). diff --git a/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsData.cs b/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsData.cs index 8cd35a1b3e1..0c7a8777495 100644 --- a/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsData.cs +++ b/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsData.cs @@ -30,8 +30,8 @@ public static List CreateRootEntities() // First basic entity with all properties set CreateRootEntity(id++, description: null), - // Second basic entity with all properties set to something different - CreateRootEntity(id++, description: "With_different_values", e => + // Second basic entity with all properties set to other values (but same across all properties) + CreateRootEntity(id++, description: "With_other_values", e => { SetRelatedValues(e.RequiredRelated); @@ -53,6 +53,7 @@ void SetRelatedValues(RelatedType related) related.RequiredNested.String = "bar"; related.OptionalNested?.Int = 9; related.OptionalNested?.String = "bar"; + foreach (var nested in related.NestedCollection) { nested.Int = 9; @@ -61,6 +62,41 @@ void SetRelatedValues(RelatedType related) } }), + // Third basic entity with all properties set to completely different values + CreateRootEntity(id++, description: "With_different_values", e => + { + var intValue = 100; + var stringValue = 100; + + SetRelatedValues(e.RequiredRelated); + + if (e.OptionalRelated is not null) + { + SetRelatedValues(e.OptionalRelated); + } + + foreach (var related in e.RelatedCollection) + { + SetRelatedValues(related); + } + + void SetRelatedValues(RelatedType related) + { + related.Int = intValue++; + related.String = $"foo{stringValue++}"; + related.RequiredNested.Int = intValue++; + related.RequiredNested.String = $"foo{stringValue++}"; + related.OptionalNested?.Int = intValue++; + related.OptionalNested?.String = $"foo{stringValue++}"; + + foreach (var nested in related.NestedCollection) + { + nested.Int = intValue++; + nested.String = $"foo{stringValue++}"; + } + } + }), + // Entity where values are referentially identical to each other across required/optional, to test various equality sceanarios. // Note that this gets overridden for owned navigations . CreateRootEntity(id++, description: "With_referential_identity", e => diff --git a/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsSetOperationsTestBase.cs b/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsSetOperationsTestBase.cs new file mode 100644 index 00000000000..cf16c6b9ce2 --- /dev/null +++ b/test/EFCore.Specification.Tests/Query/Relationships/RelationshipsSetOperationsTestBase.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships; + +public abstract class RelationshipsSetOperationsTestBase(TFixture fixture) : QueryTestBase(fixture) + where TFixture : RelationshipsQueryFixtureBase, new() +{ + [ConditionalFact] + public virtual Task On_related() + => AssertQuery( + ss => ss.Set().Where(e => + e.RelatedCollection.Where(r => r.Int == 8) + .Concat(e.RelatedCollection.Where(r => r.String == "foo")) + .Count() == 4)); + + [ConditionalTheory] + [MemberData(nameof(TrackingData))] + public virtual Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + => AssertQuery( + ss => ss.Set().Select(e => + e.RelatedCollection.Where(r => r.Int == 8).Concat(e.RelatedCollection.Where(r => r.String == "foo"))), + queryTrackingBehavior: queryTrackingBehavior); + + [ConditionalTheory] + [MemberData(nameof(TrackingData))] + public virtual Task On_related_Select_nested_with_aggregates(QueryTrackingBehavior queryTrackingBehavior) + => AssertQuery( + ss => ss.Set().Select(e => + e.RelatedCollection.Where(r => r.Int == 8) + .Concat(e.RelatedCollection.Where(r => r.String == "foo")) + .Select(r => r.NestedCollection.Select(n => n.Int).Sum()) + .Sum()), + queryTrackingBehavior: queryTrackingBehavior); + + [ConditionalFact] + public virtual Task On_nested() + => AssertQuery( + ss => ss.Set().Where(e => + e.RequiredRelated.NestedCollection.Where(r => r.Int == 8) + .Concat(e.RequiredRelated.NestedCollection.Where(r => r.String == "foo")) + .Count() == 4)); + + [ConditionalFact] + public virtual Task Over_different_collection_properties() + => AssertQuery( + ss => ss.Set().Where(e => + e.RequiredRelated.NestedCollection.Concat(e.OptionalRelated!.NestedCollection).Count() == 4), + ss => ss.Set().Where(e => + e.RequiredRelated.NestedCollection.Concat(e.OptionalRelated == null ? new() : e.OptionalRelated.NestedCollection).Count() == 4)); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonCollectionSqlServerTest.cs index bf2dd60ba84..b59b62dd8ad 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonCollectionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonCollectionSqlServerTest.cs @@ -112,6 +112,23 @@ WHERE CAST(JSON_VALUE([r].[RelatedCollection], '$[9999]') AS int) = 8 """); } + public override async Task Select_within_Select_within_Select_with_aggregates() + { + await base.Select_within_Select_within_Select_with_aggregates(); + + AssertSql( + """ +SELECT ( + SELECT COALESCE(SUM([s].[value]), 0) + FROM OPENJSON([r].[RelatedCollection], '$') WITH ([NestedCollection] nvarchar(max) '$.NestedCollection' AS JSON) AS [r0] + OUTER APPLY ( + SELECT MAX([n].[Int]) AS [value] + FROM OPENJSON([r0].[NestedCollection], '$') WITH ([Int] int '$.Int') AS [n] + ) AS [s]) +FROM [RootEntity] AS [r] +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsSqlServerTest.cs new file mode 100644 index 00000000000..ad6f8ee09b5 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsSqlServerTest.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.ComplexJson; + +public class ComplexJsonSetOperationsSqlServerTest(ComplexJsonSqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : ComplexJsonSetOperationsRelationalTestBase(fixture, testOutputHelper) +{ + public override async Task On_related() + { + await base.On_related(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalRelated], [r].[RelatedCollection], [r].[RequiredRelated] +FROM [RootEntity] AS [r] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT 1 AS empty + FROM OPENJSON([r].[RelatedCollection], '$') WITH ([Int] int '$.Int') AS [r0] + WHERE [r0].[Int] = 8 + UNION ALL + SELECT 1 AS empty + FROM OPENJSON([r].[RelatedCollection], '$') WITH ([String] nvarchar(max) '$.String') AS [r1] + WHERE [r1].[String] = N'foo' + ) AS [u]) = 4 +"""); + } + + public override async Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + { + await base.On_related_projected(queryTrackingBehavior); + + AssertSql(); + } + + public override async Task On_related_Select_nested_with_aggregates(QueryTrackingBehavior queryTrackingBehavior) + { + await base.On_related_Select_nested_with_aggregates(queryTrackingBehavior); + + AssertSql( + """ +SELECT ( + SELECT COALESCE(SUM([s].[value]), 0) + FROM ( + SELECT [r0].[NestedCollection] AS [NestedCollection] + FROM OPENJSON([r].[RelatedCollection], '$') WITH ( + [Int] int '$.Int', + [NestedCollection] nvarchar(max) '$.NestedCollection' AS JSON + ) AS [r0] + WHERE [r0].[Int] = 8 + UNION ALL + SELECT [r1].[NestedCollection] AS [NestedCollection] + FROM OPENJSON([r].[RelatedCollection], '$') WITH ( + [String] nvarchar(max) '$.String', + [NestedCollection] nvarchar(max) '$.NestedCollection' AS JSON + ) AS [r1] + WHERE [r1].[String] = N'foo' + ) AS [u] + OUTER APPLY ( + SELECT COALESCE(SUM([n].[Int]), 0) AS [value] + FROM OPENJSON([u].[NestedCollection], '$') WITH ([Int] int '$.Int') AS [n] + ) AS [s]) +FROM [RootEntity] AS [r] +"""); + } + + public override async Task On_nested() + { + await base.On_nested(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalRelated], [r].[RelatedCollection], [r].[RequiredRelated] +FROM [RootEntity] AS [r] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT 1 AS empty + FROM OPENJSON([r].[RequiredRelated], '$.NestedCollection') WITH ([Int] int '$.Int') AS [n] + WHERE [n].[Int] = 8 + UNION ALL + SELECT 1 AS empty + FROM OPENJSON([r].[RequiredRelated], '$.NestedCollection') WITH ([String] nvarchar(max) '$.String') AS [n0] + WHERE [n0].[String] = N'foo' + ) AS [u]) = 4 +"""); + } + + public override async Task Over_different_collection_properties() + { + await base.Over_different_collection_properties(); + + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/Navigations/NavigationsCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/Navigations/NavigationsCollectionSqlServerTest.cs index ec4b125ddb4..703952f46d4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/Navigations/NavigationsCollectionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/Navigations/NavigationsCollectionSqlServerTest.cs @@ -81,6 +81,25 @@ public override async Task Index_out_of_bounds() AssertSql(); } + public override async Task Select_within_Select_within_Select_with_aggregates() + { + await base.Select_within_Select_within_Select_with_aggregates(); + + AssertSql( + """ +SELECT ( + SELECT COALESCE(SUM([s].[value]), 0) + FROM [RelatedType] AS [r0] + OUTER APPLY ( + SELECT MAX([n].[Int]) AS [value] + FROM [NestedType] AS [n] + WHERE [r0].[Id] = [n].[CollectionRelatedId] + ) AS [s] + WHERE [r].[Id] = [r0].[CollectionRootId]) +FROM [RootEntity] AS [r] +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/Navigations/NavigationsSetOperationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/Navigations/NavigationsSetOperationsSqlServerTest.cs new file mode 100644 index 00000000000..495abac0493 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/Navigations/NavigationsSetOperationsSqlServerTest.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Navigations; + +public class NavigationsSetOperationsSqlServerTest( + NavigationsSqlServerFixture fixture, + ITestOutputHelper testOutputHelper) + : NavigationsSetOperationsRelationalTestBase(fixture, testOutputHelper) +{ + public override async Task On_related() + { + await base.On_related(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalRelatedId], [r].[RequiredRelatedId] +FROM [RootEntity] AS [r] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT 1 AS empty + FROM [RelatedType] AS [r0] + WHERE [r].[Id] = [r0].[CollectionRootId] AND [r0].[Int] = 8 + UNION ALL + SELECT 1 AS empty + FROM [RelatedType] AS [r1] + WHERE [r].[Id] = [r1].[CollectionRootId] AND [r1].[String] = N'foo' + ) AS [u]) = 4 +"""); + } + + public override async Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + { + await base.On_related_projected(queryTrackingBehavior); + + AssertSql(); + } + + public override async Task On_related_Select_nested_with_aggregates(QueryTrackingBehavior queryTrackingBehavior) + { + await base.On_related_Select_nested_with_aggregates(queryTrackingBehavior); + + AssertSql( + """ +SELECT ( + SELECT COALESCE(SUM([s].[value]), 0) + FROM ( + SELECT [r0].[Id] + FROM [RelatedType] AS [r0] + WHERE [r].[Id] = [r0].[CollectionRootId] AND [r0].[Int] = 8 + UNION ALL + SELECT [r1].[Id] + FROM [RelatedType] AS [r1] + WHERE [r].[Id] = [r1].[CollectionRootId] AND [r1].[String] = N'foo' + ) AS [u] + OUTER APPLY ( + SELECT COALESCE(SUM([n].[Int]), 0) AS [value] + FROM [NestedType] AS [n] + WHERE [u].[Id] = [n].[CollectionRelatedId] + ) AS [s]) +FROM [RootEntity] AS [r] +"""); + } + + public override async Task On_nested() + { + await base.On_nested(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [r].[OptionalRelatedId], [r].[RequiredRelatedId] +FROM [RootEntity] AS [r] +INNER JOIN [RelatedType] AS [r0] ON [r].[RequiredRelatedId] = [r0].[Id] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT 1 AS empty + FROM [NestedType] AS [n] + WHERE [r0].[Id] = [n].[CollectionRelatedId] AND [n].[Int] = 8 + UNION ALL + SELECT 1 AS empty + FROM [NestedType] AS [n0] + WHERE [r0].[Id] = [n0].[CollectionRelatedId] AND [n0].[String] = N'foo' + ) AS [u]) = 4 +"""); + } + + public override async Task Over_different_collection_properties() + { + await base.Over_different_collection_properties(); + + AssertSql( +""" +SELECT [r].[Id], [r].[Name], [r].[OptionalRelatedId], [r].[RequiredRelatedId] +FROM [RootEntity] AS [r] +INNER JOIN [RelatedType] AS [r0] ON [r].[RequiredRelatedId] = [r0].[Id] +LEFT JOIN [RelatedType] AS [r1] ON [r].[OptionalRelatedId] = [r1].[Id] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT 1 AS empty + FROM [NestedType] AS [n] + WHERE [r0].[Id] = [n].[CollectionRelatedId] + UNION ALL + SELECT 1 AS empty + FROM [NestedType] AS [n0] + WHERE [r1].[Id] IS NOT NULL AND [r1].[Id] = [n0].[CollectionRelatedId] + ) AS [u]) = 4 +"""); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedJson/OwnedJsonCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedJson/OwnedJsonCollectionSqlServerTest.cs index 29a6ee12525..32fcafcf859 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedJson/OwnedJsonCollectionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedJson/OwnedJsonCollectionSqlServerTest.cs @@ -73,6 +73,8 @@ ORDER BY [r0].[Id] """); } + #region Index + public override async Task Index_parameter() { await base.Index_parameter(); @@ -111,6 +113,30 @@ WHERE CAST(JSON_VALUE([r].[RelatedCollection], '$[9999].Int') AS int) = 8 """); } + #endregion Index + + public override async Task Select_within_Select_within_Select_with_aggregates() + { + await base.Select_within_Select_within_Select_with_aggregates(); + + AssertSql( + """ +SELECT ( + SELECT COALESCE(SUM([s].[value]), 0) + FROM OPENJSON([r].[RelatedCollection], '$') WITH ([NestedCollection] nvarchar(max) '$.NestedCollection' AS JSON) AS [r0] + OUTER APPLY ( + SELECT MAX([n].[Int]) AS [value] + FROM OPENJSON([r0].[NestedCollection], '$') WITH ( + [Id] int '$.Id', + [Int] int '$.Int', + [Name] nvarchar(max) '$.Name', + [String] nvarchar(max) '$.String' + ) AS [n] + ) AS [s]) +FROM [RootEntity] AS [r] +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs index 7cb09c63edc..b46241a2f39 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsCollectionSqlServerTest.cs @@ -235,6 +235,25 @@ ORDER BY (SELECT 1) """); } + public override async Task Select_within_Select_within_Select_with_aggregates() + { + await base.Select_within_Select_within_Select_with_aggregates(); + + AssertSql( + """ +SELECT ( + SELECT COALESCE(SUM([s].[value]), 0) + FROM [RelatedCollection] AS [r0] + OUTER APPLY ( + SELECT MAX([r1].[Int]) AS [value] + FROM [RelatedCollection_NestedCollection] AS [r1] + WHERE [r0].[RootEntityId] = [r1].[RelatedTypeRootEntityId] AND [r0].[Id] = [r1].[RelatedTypeId] + ) AS [s] + WHERE [r].[Id] = [r0].[RootEntityId]) +FROM [RootEntity] AS [r] +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsSqlServerTest.cs new file mode 100644 index 00000000000..f1e736cbb91 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsSqlServerTest.cs @@ -0,0 +1,126 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.OwnedNavigations; + +public class OwnedNavigationsSetOperationsSqlServerTest( + OwnedNavigationsSqlServerFixture fixture, + ITestOutputHelper testOutputHelper) + : OwnedNavigationsSetOperationsRelationalTestBase(fixture, testOutputHelper) +{ + public override async Task On_related() + { + await base.On_related(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [o].[RootEntityId], [o].[Id], [o].[Int], [o].[Name], [o].[String], [o0].[RelatedTypeRootEntityId], [o1].[RelatedTypeRootEntityId], [r2].[RootEntityId], [r3].[RelatedTypeRootEntityId], [r4].[RelatedTypeRootEntityId], [o2].[RelatedTypeRootEntityId], [o2].[Id], [o2].[Int], [o2].[Name], [o2].[String], [o0].[Id], [o0].[Int], [o0].[Name], [o0].[String], [o1].[Id], [o1].[Int], [o1].[Name], [o1].[String], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Name], [s].[String], [s].[RelatedTypeRootEntityId], [s].[RelatedTypeId], [s].[RelatedTypeRootEntityId0], [s].[RelatedTypeId0], [s].[RelatedTypeRootEntityId1], [s].[RelatedTypeId1], [s].[Id0], [s].[Int0], [s].[Name0], [s].[String0], [s].[Id1], [s].[Int1], [s].[Name1], [s].[String1], [s].[Id2], [s].[Int2], [s].[Name2], [s].[String2], [r2].[Id], [r2].[Int], [r2].[Name], [r2].[String], [r9].[RelatedTypeRootEntityId], [r9].[Id], [r9].[Int], [r9].[Name], [r9].[String], [r3].[Id], [r3].[Int], [r3].[Name], [r3].[String], [r4].[Id], [r4].[Int], [r4].[Name], [r4].[String] +FROM [RootEntity] AS [r] +LEFT JOIN [OptionalRelated] AS [o] ON [r].[Id] = [o].[RootEntityId] +LEFT JOIN [OptionalRelated_OptionalNested] AS [o0] ON [o].[RootEntityId] = [o0].[RelatedTypeRootEntityId] +LEFT JOIN [OptionalRelated_RequiredNested] AS [o1] ON [o].[RootEntityId] = [o1].[RelatedTypeRootEntityId] +LEFT JOIN [RequiredRelated] AS [r2] ON [r].[Id] = [r2].[RootEntityId] +LEFT JOIN [RequiredRelated_OptionalNested] AS [r3] ON [r2].[RootEntityId] = [r3].[RelatedTypeRootEntityId] +LEFT JOIN [RequiredRelated_RequiredNested] AS [r4] ON [r2].[RootEntityId] = [r4].[RelatedTypeRootEntityId] +LEFT JOIN [OptionalRelated_NestedCollection] AS [o2] ON [o].[RootEntityId] = [o2].[RelatedTypeRootEntityId] +LEFT JOIN ( + SELECT [r5].[RootEntityId], [r5].[Id], [r5].[Int], [r5].[Name], [r5].[String], [r6].[RelatedTypeRootEntityId], [r6].[RelatedTypeId], [r7].[RelatedTypeRootEntityId] AS [RelatedTypeRootEntityId0], [r7].[RelatedTypeId] AS [RelatedTypeId0], [r8].[RelatedTypeRootEntityId] AS [RelatedTypeRootEntityId1], [r8].[RelatedTypeId] AS [RelatedTypeId1], [r8].[Id] AS [Id0], [r8].[Int] AS [Int0], [r8].[Name] AS [Name0], [r8].[String] AS [String0], [r6].[Id] AS [Id1], [r6].[Int] AS [Int1], [r6].[Name] AS [Name1], [r6].[String] AS [String1], [r7].[Id] AS [Id2], [r7].[Int] AS [Int2], [r7].[Name] AS [Name2], [r7].[String] AS [String2] + FROM [RelatedCollection] AS [r5] + LEFT JOIN [RelatedCollection_OptionalNested] AS [r6] ON [r5].[RootEntityId] = [r6].[RelatedTypeRootEntityId] AND [r5].[Id] = [r6].[RelatedTypeId] + LEFT JOIN [RelatedCollection_RequiredNested] AS [r7] ON [r5].[RootEntityId] = [r7].[RelatedTypeRootEntityId] AND [r5].[Id] = [r7].[RelatedTypeId] + LEFT JOIN [RelatedCollection_NestedCollection] AS [r8] ON [r5].[RootEntityId] = [r8].[RelatedTypeRootEntityId] AND [r5].[Id] = [r8].[RelatedTypeId] +) AS [s] ON [r].[Id] = [s].[RootEntityId] +LEFT JOIN [RequiredRelated_NestedCollection] AS [r9] ON [r2].[RootEntityId] = [r9].[RelatedTypeRootEntityId] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT 1 AS empty + FROM [RelatedCollection] AS [r0] + WHERE [r].[Id] = [r0].[RootEntityId] AND [r0].[Int] = 8 + UNION ALL + SELECT 1 AS empty + FROM [RelatedCollection] AS [r1] + WHERE [r].[Id] = [r1].[RootEntityId] AND [r1].[String] = N'foo' + ) AS [u]) = 4 +ORDER BY [r].[Id], [o].[RootEntityId], [o0].[RelatedTypeRootEntityId], [o1].[RelatedTypeRootEntityId], [r2].[RootEntityId], [r3].[RelatedTypeRootEntityId], [r4].[RelatedTypeRootEntityId], [o2].[RelatedTypeRootEntityId], [o2].[Id], [s].[RootEntityId], [s].[Id], [s].[RelatedTypeRootEntityId], [s].[RelatedTypeId], [s].[RelatedTypeRootEntityId0], [s].[RelatedTypeId0], [s].[RelatedTypeRootEntityId1], [s].[RelatedTypeId1], [s].[Id0], [r9].[RelatedTypeRootEntityId] +"""); + } + + public override Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + => Assert.ThrowsAnyAsync(() => base.On_related_projected(queryTrackingBehavior)); + + public override async Task On_related_Select_nested_with_aggregates(QueryTrackingBehavior queryTrackingBehavior) + { + await base.On_related_Select_nested_with_aggregates(queryTrackingBehavior); + + AssertSql( + """ +SELECT ( + SELECT COALESCE(SUM([s].[value]), 0) + FROM ( + SELECT [r0].[RootEntityId], [r0].[Id] + FROM [RelatedCollection] AS [r0] + WHERE [r].[Id] = [r0].[RootEntityId] AND [r0].[Int] = 8 + UNION ALL + SELECT [r1].[RootEntityId], [r1].[Id] + FROM [RelatedCollection] AS [r1] + WHERE [r].[Id] = [r1].[RootEntityId] AND [r1].[String] = N'foo' + ) AS [u] + OUTER APPLY ( + SELECT COALESCE(SUM([r2].[Int]), 0) AS [value] + FROM [RelatedCollection_NestedCollection] AS [r2] + WHERE [u].[RootEntityId] = [r2].[RelatedTypeRootEntityId] AND [u].[Id] = [r2].[RelatedTypeId] + ) AS [s]) +FROM [RootEntity] AS [r] +"""); + } + + public override async Task On_nested() + { + await base.On_nested(); + + AssertSql( + """ +SELECT [r].[Id], [r].[Name], [o].[RootEntityId], [o].[Id], [o].[Int], [o].[Name], [o].[String], [r0].[RootEntityId], [o0].[RelatedTypeRootEntityId], [o1].[RelatedTypeRootEntityId], [r3].[RelatedTypeRootEntityId], [r4].[RelatedTypeRootEntityId], [o2].[RelatedTypeRootEntityId], [o2].[Id], [o2].[Int], [o2].[Name], [o2].[String], [o0].[Id], [o0].[Int], [o0].[Name], [o0].[String], [o1].[Id], [o1].[Int], [o1].[Name], [o1].[String], [s].[RootEntityId], [s].[Id], [s].[Int], [s].[Name], [s].[String], [s].[RelatedTypeRootEntityId], [s].[RelatedTypeId], [s].[RelatedTypeRootEntityId0], [s].[RelatedTypeId0], [s].[RelatedTypeRootEntityId1], [s].[RelatedTypeId1], [s].[Id0], [s].[Int0], [s].[Name0], [s].[String0], [s].[Id1], [s].[Int1], [s].[Name1], [s].[String1], [s].[Id2], [s].[Int2], [s].[Name2], [s].[String2], [r0].[Id], [r0].[Int], [r0].[Name], [r0].[String], [r9].[RelatedTypeRootEntityId], [r9].[Id], [r9].[Int], [r9].[Name], [r9].[String], [r3].[Id], [r3].[Int], [r3].[Name], [r3].[String], [r4].[Id], [r4].[Int], [r4].[Name], [r4].[String] +FROM [RootEntity] AS [r] +LEFT JOIN [RequiredRelated] AS [r0] ON [r].[Id] = [r0].[RootEntityId] +LEFT JOIN [OptionalRelated] AS [o] ON [r].[Id] = [o].[RootEntityId] +LEFT JOIN [OptionalRelated_OptionalNested] AS [o0] ON [o].[RootEntityId] = [o0].[RelatedTypeRootEntityId] +LEFT JOIN [OptionalRelated_RequiredNested] AS [o1] ON [o].[RootEntityId] = [o1].[RelatedTypeRootEntityId] +LEFT JOIN [RequiredRelated_OptionalNested] AS [r3] ON [r0].[RootEntityId] = [r3].[RelatedTypeRootEntityId] +LEFT JOIN [RequiredRelated_RequiredNested] AS [r4] ON [r0].[RootEntityId] = [r4].[RelatedTypeRootEntityId] +LEFT JOIN [OptionalRelated_NestedCollection] AS [o2] ON [o].[RootEntityId] = [o2].[RelatedTypeRootEntityId] +LEFT JOIN ( + SELECT [r5].[RootEntityId], [r5].[Id], [r5].[Int], [r5].[Name], [r5].[String], [r6].[RelatedTypeRootEntityId], [r6].[RelatedTypeId], [r7].[RelatedTypeRootEntityId] AS [RelatedTypeRootEntityId0], [r7].[RelatedTypeId] AS [RelatedTypeId0], [r8].[RelatedTypeRootEntityId] AS [RelatedTypeRootEntityId1], [r8].[RelatedTypeId] AS [RelatedTypeId1], [r8].[Id] AS [Id0], [r8].[Int] AS [Int0], [r8].[Name] AS [Name0], [r8].[String] AS [String0], [r6].[Id] AS [Id1], [r6].[Int] AS [Int1], [r6].[Name] AS [Name1], [r6].[String] AS [String1], [r7].[Id] AS [Id2], [r7].[Int] AS [Int2], [r7].[Name] AS [Name2], [r7].[String] AS [String2] + FROM [RelatedCollection] AS [r5] + LEFT JOIN [RelatedCollection_OptionalNested] AS [r6] ON [r5].[RootEntityId] = [r6].[RelatedTypeRootEntityId] AND [r5].[Id] = [r6].[RelatedTypeId] + LEFT JOIN [RelatedCollection_RequiredNested] AS [r7] ON [r5].[RootEntityId] = [r7].[RelatedTypeRootEntityId] AND [r5].[Id] = [r7].[RelatedTypeId] + LEFT JOIN [RelatedCollection_NestedCollection] AS [r8] ON [r5].[RootEntityId] = [r8].[RelatedTypeRootEntityId] AND [r5].[Id] = [r8].[RelatedTypeId] +) AS [s] ON [r].[Id] = [s].[RootEntityId] +LEFT JOIN [RequiredRelated_NestedCollection] AS [r9] ON [r0].[RootEntityId] = [r9].[RelatedTypeRootEntityId] +WHERE ( + SELECT COUNT(*) + FROM ( + SELECT 1 AS empty + FROM [RequiredRelated_NestedCollection] AS [r1] + WHERE [r0].[RootEntityId] = [r1].[RelatedTypeRootEntityId] AND [r1].[Int] = 8 + UNION ALL + SELECT 1 AS empty + FROM [RequiredRelated_NestedCollection] AS [r2] + WHERE [r0].[RootEntityId] = [r2].[RelatedTypeRootEntityId] AND [r2].[String] = N'foo' + ) AS [u]) = 4 +ORDER BY [r].[Id], [r0].[RootEntityId], [o].[RootEntityId], [o0].[RelatedTypeRootEntityId], [o1].[RelatedTypeRootEntityId], [r3].[RelatedTypeRootEntityId], [r4].[RelatedTypeRootEntityId], [o2].[RelatedTypeRootEntityId], [o2].[Id], [s].[RootEntityId], [s].[Id], [s].[RelatedTypeRootEntityId], [s].[RelatedTypeId], [s].[RelatedTypeRootEntityId0], [s].[RelatedTypeId0], [s].[RelatedTypeRootEntityId1], [s].[RelatedTypeId1], [s].[Id0], [r9].[RelatedTypeRootEntityId] +"""); + } + + public override async Task Over_different_collection_properties() + { + await base.Over_different_collection_properties(); + + AssertSql(); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsSqliteTest.cs new file mode 100644 index 00000000000..72924792150 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/ComplexJson/ComplexJsonSetOperationsSqliteTest.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.ComplexJson; + +public class ComplexJsonSetOperationsSqliteTest(ComplexJsonSqliteFixture fixture, ITestOutputHelper testOutputHelper) + : ComplexJsonSetOperationsRelationalTestBase(fixture, testOutputHelper) +{ + +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/Navigations/NavigationsSetOperationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/Navigations/NavigationsSetOperationsSqliteTest.cs new file mode 100644 index 00000000000..2ee2792fe0a --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/Navigations/NavigationsSetOperationsSqliteTest.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.Navigations; + +public class NavigationsSetOperationsSqliteTest(NavigationsSqliteFixture fixture, ITestOutputHelper testOutputHelper) + : NavigationsSetOperationsRelationalTestBase(fixture, testOutputHelper) +{ +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsSqliteTest.cs new file mode 100644 index 00000000000..2af24c8d0c7 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/Relationships/OwnedNavigations/OwnedNavigationsSetOperationsSqliteTest.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit.Sdk; + +namespace Microsoft.EntityFrameworkCore.Query.Relationships.OwnedNavigations; + +public class OwnedNavigationsSetOperationsSqliteTest(OwnedNavigationsSqliteFixture fixture, ITestOutputHelper testOutputHelper) + : OwnedNavigationsSetOperationsRelationalTestBase(fixture, testOutputHelper) +{ + // SQL APPLY not supported in SQLite - different exception message from the one expected in the base class + public override Task On_related_projected(QueryTrackingBehavior queryTrackingBehavior) + => Assert.ThrowsAsync(() => base.On_related_projected(queryTrackingBehavior)); +}