Skip to content

Commit

Permalink
Fix to #29156 - TemporalAll for temporal owned entities mapped to par…
Browse files Browse the repository at this point in the history
…ent table. (#32031)

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
  • Loading branch information
maumar authored Oct 12, 2023
1 parent 8e4513e commit d0055a4
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public override EntityQueryRootExpression CreateQueryRoot(IEntityType entityType
/// </summary>
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())
{
Expand All @@ -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;

/// <summary>
/// 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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.SqlServer.Internal;

namespace Microsoft.EntityFrameworkCore.Query;

[SqlServerCondition(SqlServerCondition.SupportsTemporalTablesCascadeDelete)]
Expand Down Expand Up @@ -37,6 +39,69 @@ 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<OwnedPerson>()
.TemporalAsOf(new DateTime(2010, 1, 1));

var resultAsOf = async
? await queryAsOf.ToListAsync()
: queryAsOf.ToList();

var queryAll = context.Set<OwnedPerson>()
.TemporalAll()
.Select(x => x.PersonAddress);

var resultAll = async
? await queryAll.ToListAsync()
: queryAll.ToList();

var queryBetween = context.Set<OwnedPerson>()
.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]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Navigation_on_owned_entity_mapped_to_different_table_fails_for_non_asof(bool async)
{
var context = CreateContext();
var message = (await Assert.ThrowsAsync<InvalidOperationException>(
() => context.Set<Star>().TemporalAll().Select(x => x.Planets.ToList()).ToListAsync())).Message;

Assert.Equal(SqlServerStrings.TemporalNavigationExpansionOnlySupportedForAsOf("AsOf"), message);
}

public override async Task Query_with_owned_entity_equality_operator(bool async)
{
await base.Query_with_owned_entity_equality_operator(async);
Expand Down

0 comments on commit d0055a4

Please sign in to comment.