From 22e858043baee5be2af74bd4de534fa637adc719 Mon Sep 17 00:00:00 2001 From: maumar Date: Wed, 11 Oct 2023 16:22:20 -0700 Subject: [PATCH] Fix to #29156 - TemporalAll for temporal owned entities mapped to parent table. Similar to JSON entities, owned entities that are mapped to the same table as their owner should be treated as scalars for the purpose of temporal query validation - they are always in sync with the parent entity, so all operations should be allowed for them, not only AsOf. Fixes #29156 --- ...rNavigationExpansionExtensibilityHelper.cs | 10 +++- .../Query/TemporalOwnedQuerySqlServerTest.cs | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs index cfcfb17f282..72ae2e2ca6f 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerNavigationExpansionExtensibilityHelper.cs @@ -51,7 +51,9 @@ public override EntityQueryRootExpression CreateQueryRoot(IEntityType entityType /// public override void ValidateQueryRootCreation(IEntityType entityType, EntityQueryRootExpression? source) { - if (source is TemporalQueryRootExpression && !entityType.IsMappedToJson()) + if (source is TemporalQueryRootExpression + && !entityType.IsMappedToJson() + && !OwnedEntityMappedToSameTableAsOwner(entityType)) { if (!entityType.GetRootType().IsTemporal()) { @@ -69,6 +71,12 @@ public override void ValidateQueryRootCreation(IEntityType entityType, EntityQue base.ValidateQueryRootCreation(entityType, source); } + private bool OwnedEntityMappedToSameTableAsOwner(IEntityType entityType) + => entityType.IsOwned() + && entityType.FindOwnership()!.PrincipalEntityType.GetTableMappings().FirstOrDefault()?.Table is ITable ownerTable + && entityType.GetTableMappings().FirstOrDefault()?.Table is ITable entityTable + && ownerTable == entityTable; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs index da0f7a476f8..a996f7d0e13 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalOwnedQuerySqlServerTest.cs @@ -37,6 +37,58 @@ protected override Expression RewriteServerQueryExpression(Expression serverQuer return rewriter.Visit(serverQueryExpression); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Navigation_on_owned_entity_mapped_to_same_table_works_with_all_temporal_methods(bool async) + { + var context = CreateContext(); + + var queryAsOf = context.Set() + .TemporalAsOf(new DateTime(2010, 1, 1)); + + var resultAsOf = async + ? await queryAsOf.ToListAsync() + : queryAsOf.ToList(); + + var queryAll = context.Set() + .TemporalAll() + .Select(x => x.PersonAddress); + + var resultAll = async + ? await queryAll.ToListAsync() + : queryAll.ToList(); + + var queryBetween = context.Set() + .TemporalBetween(new DateTime(1990, 1, 1), new DateTime(2200, 1, 1)) + .Select(x => x.PersonAddress); + + var resultBetween = async + ? await queryBetween.ToListAsync() + : queryBetween.ToList(); + + AssertSql( +""" +SELECT [o].[Id], [o].[Discriminator], [o].[Name], [o].[PeriodEnd], [o].[PeriodStart], [t].[ClientId], [t].[Id], [t].[OrderDate], [t].[PeriodEnd], [t].[PeriodStart], [t].[OrderClientId], [t].[OrderId], [t].[Id0], [t].[Detail], [t].[PeriodEnd0], [t].[PeriodStart0], [o].[PersonAddress_AddressLine], [o].[PeriodEnd], [o].[PeriodStart], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] +FROM [OwnedPerson] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [o] +LEFT JOIN ( + SELECT [o0].[ClientId], [o0].[Id], [o0].[OrderDate], [o0].[PeriodEnd], [o0].[PeriodStart], [o1].[OrderClientId], [o1].[OrderId], [o1].[Id] AS [Id0], [o1].[Detail], [o1].[PeriodEnd] AS [PeriodEnd0], [o1].[PeriodStart] AS [PeriodStart0] + FROM [Order] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [o0] + LEFT JOIN [OrderDetail] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [o1] ON [o0].[ClientId] = [o1].[OrderClientId] AND [o0].[Id] = [o1].[OrderId] +) AS [t] ON [o].[Id] = [t].[ClientId] +ORDER BY [o].[Id], [t].[ClientId], [t].[Id], [t].[OrderClientId], [t].[OrderId] +""", + // + """ +SELECT [o].[Id], [o].[PersonAddress_AddressLine], [o].[PeriodEnd], [o].[PeriodStart], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId] +FROM [OwnedPerson] FOR SYSTEM_TIME ALL AS [o] +""", + // + """ +SELECT [o].[Id], [o].[PersonAddress_AddressLine], [o].[PeriodEnd], [o].[PeriodStart], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId] +FROM [OwnedPerson] FOR SYSTEM_TIME BETWEEN '1990-01-01T00:00:00.0000000' AND '2200-01-01T00:00:00.0000000' AS [o] +"""); + } + public override async Task Query_with_owned_entity_equality_operator(bool async) { await base.Query_with_owned_entity_equality_operator(async);