From 3782d32ffdbfd48159298c7baf7f2ceb5ec8fe54 Mon Sep 17 00:00:00 2001 From: maumar Date: Wed, 3 Nov 2021 03:53:38 -0700 Subject: [PATCH] Fix to #26451 - Temporal Table: Owned Entities support Added extensibility point to SharedTypeEntityExpandingExpressionVisitor so that we can create temporal table expressions representing owned entities, if their owner is also a temporal table expression. Just like any other nav expansion, this only works for AsOf operation. --- ...ntityFrameworkRelationalServicesBuilder.cs | 5 +- ...lationalSharedTypeEntityExpansionHelper.cs | 32 ++ ...yableMethodTranslatingExpressionVisitor.cs | 52 ++- ...ranslatingExpressionVisitorDependencies.cs | 10 +- ...lationalSharedTypeEntityExpansionHelper.cs | 32 ++ ...edTypeEntityExpansionHelperDependencies.cs | 67 ++++ .../SqlServerServiceCollectionExtensions.cs | 1 + .../Properties/SqlServerStrings.Designer.cs | 8 + .../Properties/SqlServerStrings.resx | 3 + ...qlServerSharedTypeEntityExpansionHelper.cs | 49 +++ .../Query/QueryBugsTest.cs | 1 - .../Query/TemporalTableSqlServerTest.cs | 300 ++++++++++++++++++ 12 files changed, 553 insertions(+), 7 deletions(-) create mode 100644 src/EFCore.Relational/Query/IRelationalSharedTypeEntityExpansionHelper.cs create mode 100644 src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs create mode 100644 src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs create mode 100644 src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index c6b92dce01a..d8145062e3f 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -81,6 +81,7 @@ public static readonly IDictionary RelationalServi { typeof(ISqlExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IRelationalQueryStringFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IRelationalParameterBasedSqlProcessorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IRelationalSharedTypeEntityExpansionHelper), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsModelDiffer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -192,6 +193,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); ServiceCollectionMap.GetInfrastructure() .AddDependencySingleton() @@ -227,7 +229,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() - .AddDependencyScoped(); + .AddDependencyScoped() + .AddDependencyScoped(); return base.TryAddCoreServices(); } diff --git a/src/EFCore.Relational/Query/IRelationalSharedTypeEntityExpansionHelper.cs b/src/EFCore.Relational/Query/IRelationalSharedTypeEntityExpansionHelper.cs new file mode 100644 index 00000000000..5f4eef1ab11 --- /dev/null +++ b/src/EFCore.Relational/Query/IRelationalSharedTypeEntityExpansionHelper.cs @@ -0,0 +1,32 @@ +// 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.Metadata; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// Service which helps with various aspects of shared type entity expansion extensibility for relational providrers. + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + /// See Implementation of database providers and extensions + /// and How EF Core queries work for more information. + /// + public interface IRelationalSharedTypeEntityExpansionHelper + { + /// + /// Creates a SelectExpression representing owned type. + /// + public SelectExpression CreateInnerSelectExpression( + TableExpressionBase sourceTable, + IEntityType targetEntityType); + } +} diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 9adff93a033..e89df3e21ac 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -50,7 +50,10 @@ public RelationalQueryableMethodTranslatingExpressionVisitor( _queryCompilationContext = queryCompilationContext; _sqlTranslator = relationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(queryCompilationContext, this); _sharedTypeEntityExpandingExpressionVisitor = - new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory); + new SharedTypeEntityExpandingExpressionVisitor( + _sqlTranslator, + sqlExpressionFactory, + relationalDependencies.RelationalSharedTypeEntityExpansionHelper); _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); _sqlExpressionFactory = sqlExpressionFactory; _subquery = false; @@ -74,7 +77,11 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor( _sqlTranslator = RelationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create( parentVisitor._queryCompilationContext, parentVisitor); _sharedTypeEntityExpandingExpressionVisitor = - new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory); + new SharedTypeEntityExpandingExpressionVisitor( + _sqlTranslator, + parentVisitor._sqlExpressionFactory, + RelationalDependencies.RelationalSharedTypeEntityExpansionHelper); + _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; _subquery = true; @@ -1144,15 +1151,18 @@ private static readonly MethodInfo _objectEqualsMethodInfo private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IRelationalSharedTypeEntityExpansionHelper _sharedTypeEntityExpansionHelper; private SelectExpression _selectExpression; public SharedTypeEntityExpandingExpressionVisitor( RelationalSqlTranslatingExpressionVisitor sqlTranslator, - ISqlExpressionFactory sqlExpressionFactory) + ISqlExpressionFactory sqlExpressionFactory, + IRelationalSharedTypeEntityExpansionHelper sharedTypeEntityExpansionHelper) { _sqlTranslator = sqlTranslator; _sqlExpressionFactory = sqlExpressionFactory; + _sharedTypeEntityExpansionHelper = sharedTypeEntityExpansionHelper; _selectExpression = null!; } @@ -1341,7 +1351,20 @@ outerKey is NewArrayExpression newArrayExpression // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 // So there is no handling for dependent having TPT table = targetEntityType.GetViewOrTableMappings().Single().Table; - var innerSelectExpression = _sqlExpressionFactory.Select(targetEntityType); + + // just need any column - we use it only to extract the table it originated from + var sourceColumn = entityProjectionExpression + .BindProperty( + navigation.IsOnDependent + ? foreignKey.Properties[0] + : foreignKey.PrincipalKey.Properties[0]); + + var sourceTable = FindRootTableExpressionForColumn(sourceColumn.Table, sourceColumn.Name); + + var innerSelectExpression = _sharedTypeEntityExpansionHelper.CreateInnerSelectExpression( + sourceTable, + targetEntityType); + var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); var makeNullable = foreignKey.PrincipalKey.Properties @@ -1375,6 +1398,27 @@ outerKey is NewArrayExpression newArrayExpression } return innerShaper; + + TableExpressionBase FindRootTableExpressionForColumn(TableExpressionBase table, string columnName) + { + if (table is JoinExpressionBase joinExpressionBase) + { + table = joinExpressionBase.Table; + } + else if (table is SetOperationBase setOperationBase) + { + table = setOperationBase.Source1; + } + + if (table is SelectExpression selectExpression) + { + var matchingProjection = (ColumnExpression)selectExpression.Projection.Where(p => p.Alias == columnName).Single().Expression; + + return FindRootTableExpressionForColumn(matchingProjection.Table, matchingProjection.Name); + } + + return table; + } } private static Expression AddConvertToObject(Expression expression) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs index 89f166d6976..f296b250994 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs @@ -54,13 +54,16 @@ public sealed record RelationalQueryableMethodTranslatingExpressionVisitorDepend [EntityFrameworkInternal] public RelationalQueryableMethodTranslatingExpressionVisitorDependencies( IRelationalSqlTranslatingExpressionVisitorFactory relationalSqlTranslatingExpressionVisitorFactory, - ISqlExpressionFactory sqlExpressionFactory) + ISqlExpressionFactory sqlExpressionFactory, + IRelationalSharedTypeEntityExpansionHelper relationalSharedTypeEntityExpansionHelper) { Check.NotNull(relationalSqlTranslatingExpressionVisitorFactory, nameof(relationalSqlTranslatingExpressionVisitorFactory)); Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); + Check.NotNull(relationalSharedTypeEntityExpansionHelper, nameof(relationalSharedTypeEntityExpansionHelper)); RelationalSqlTranslatingExpressionVisitorFactory = relationalSqlTranslatingExpressionVisitorFactory; SqlExpressionFactory = sqlExpressionFactory; + RelationalSharedTypeEntityExpansionHelper = relationalSharedTypeEntityExpansionHelper; } /// @@ -72,5 +75,10 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies( /// The SQL expression factory. /// public ISqlExpressionFactory SqlExpressionFactory { get; init; } + + /// + /// Shared type entity expansion helper. + /// + public IRelationalSharedTypeEntityExpansionHelper RelationalSharedTypeEntityExpansionHelper { get; init; } } } diff --git a/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs new file mode 100644 index 00000000000..7df6137820c --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelper.cs @@ -0,0 +1,32 @@ +// 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.Metadata; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + public class RelationalSharedTypeEntityExpansionHelper : IRelationalSharedTypeEntityExpansionHelper + { + /// + /// Creates a new instance of the class. + /// + /// Dependencies for this service. + public RelationalSharedTypeEntityExpansionHelper(RelationalSharedTypeEntityExpansionHelperDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual RelationalSharedTypeEntityExpansionHelperDependencies Dependencies { get; } + + /// + public virtual SelectExpression CreateInnerSelectExpression( + TableExpressionBase sourceTable, + IEntityType targetEntityType) + => Dependencies.SqlExpressionFactory.Select(targetEntityType); + } +} diff --git a/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs new file mode 100644 index 00000000000..8ae0349dc65 --- /dev/null +++ b/src/EFCore.Relational/Query/RelationalSharedTypeEntityExpansionHelperDependencies.cs @@ -0,0 +1,67 @@ +// 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.Infrastructure; +using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// + /// Service dependencies parameter class for + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// Do not construct instances of this class directly from either provider or application code as the + /// constructor signature may change as new dependencies are added. Instead, use this type in + /// your constructor so that an instance will be created and injected automatically by the + /// dependency injection container. To create an instance with some dependent services replaced, + /// first resolve the object from the dependency injection container, then replace selected + /// services using the 'With...' methods. Do not call the constructor at any point in this process. + /// + /// + /// The service lifetime is . This means that each + /// instance will use its own instance of this service. + /// The implementation may depend on other services registered with any lifetime. + /// The implementation does not need to be thread-safe. + /// + /// + public sealed record RelationalSharedTypeEntityExpansionHelperDependencies + { + /// + /// + /// Creates the service dependencies parameter object for a . + /// + /// + /// Do not call this constructor directly from either provider or application code as it may change + /// as new dependencies are added. Instead, use this type in your constructor so that an instance + /// will be created and injected automatically by the dependency injection container. To create + /// an instance with some dependent services replaced, first resolve the object from the dependency + /// injection container, then replace selected services using the 'With...' methods. Do not call + /// the constructor at any point in this process. + /// + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + [EntityFrameworkInternal] + public RelationalSharedTypeEntityExpansionHelperDependencies(ISqlExpressionFactory sqlExpressionFactory) + { + Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); + + SqlExpressionFactory = sqlExpressionFactory; + } + + /// + /// The SQL expression factory. + /// + public ISqlExpressionFactory SqlExpressionFactory { get; init; } + } +} diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 1b36617aa96..1130d9d8a63 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -142,6 +142,7 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAddProviderSpecificServices( b => b .TryAddSingleton() diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index 23fd122e7e9..ba978d7792f 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -331,6 +331,14 @@ public static string TemporalSetOperationOnMismatchedSources(object? entityType) GetString("TemporalSetOperationOnMismatchedSources", nameof(entityType)), entityType); + /// + /// Only '{operationName}' temporal operation is supported for entitiy that owns another entity which is mapped different table. + /// + public static string TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf(object? operationName) + => string.Format( + GetString("TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf", nameof(operationName)), + operationName); + /// /// An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 5770c19ea28..464cda3061d 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -325,6 +325,9 @@ Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match. + + Only '{operationName}' temporal operation is supported for entitiy that owns another entity which is mapped different table. + An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs new file mode 100644 index 00000000000..d6350852442 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSharedTypeEntityExpansionHelper.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + public class SqlServerSharedTypeEntityExpansionHelper : RelationalSharedTypeEntityExpansionHelper + { + /// + /// 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerSharedTypeEntityExpansionHelper(RelationalSharedTypeEntityExpansionHelperDependencies dependencies) + : base(dependencies) + { + } + + /// + public override SelectExpression CreateInnerSelectExpression( + TableExpressionBase sourceTable, + IEntityType targetEntityType) + { + if (sourceTable is TemporalAsOfTableExpression temporalAsOf) + { + var table = targetEntityType.GetTableMappings().Single().Table; + var temporalTableExpression = new TemporalAsOfTableExpression(table, temporalAsOf.PointInTime); + + return Dependencies.SqlExpressionFactory.Select(targetEntityType, temporalTableExpression); + } + + if (sourceTable is TemporalTableExpression) + { + throw new InvalidOperationException( + SqlServerStrings.TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf("AsOf")); + } + + return base.CreateInnerSelectExpression(sourceTable, targetEntityType); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 4aa24d68af7..75e3ec2a797 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -18,7 +18,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs new file mode 100644 index 00000000000..38fba7261a0 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalTableSqlServerTest.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Query +{ + [SqlServerCondition(SqlServerCondition.SupportsTemporalTablesCascadeDelete)] + public class TemporalTableSqlServerTest : NonSharedModelTestBase + { + protected override string StoreName => "TemporalTableSqlServerTest"; + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_basic(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities.TemporalAsOf(date); + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"SELECT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime], [o].[MainEntityId], [o].[Description], [o].[EndTime], [o].[StartTime] +FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [m].[Id] = [o].[MainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_join(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .TemporalAsOf(date) + .Join(context.MainEntities, o => o.Id, i => i.Id, (o, i) => new { o, i }); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"SELECT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime], [o].[MainEntityId], [o].[Description], [o].[EndTime], [o].[StartTime], [m0].[Id], [m0].[Description], [m0].[EndTime], [m0].[StartTime], [o0].[MainEntityId], [o0].[Description], [o0].[EndTime], [o0].[StartTime] +FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] +INNER JOIN [MainEntity] AS [m0] ON [m].[Id] = [m0].[Id] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [m].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntity] AS [o0] ON [m0].[Id] = [o0].[MainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_set_operation(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .TemporalAsOf(date) + .Union(context.MainEntities.TemporalAsOf(date)); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"SELECT [t].[Id], [t].[Description], [t].[EndTime], [t].[StartTime], [o].[MainEntityId], [o].[Description], [o].[EndTime], [o].[StartTime] +FROM ( + SELECT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] + UNION + SELECT [m0].[Id], [m0].[Description], [m0].[EndTime], [m0].[StartTime] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m0] +) AS [t] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [t].[Id] = [o].[MainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_FromSql(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities.FromSqlRaw( + @"SELECT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime] +FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m]"); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + // just making sure we don't do anything weird here - there is no way to extract temporal information + // from the FromSql so owned entity will always be treated as a regular query + AssertSql( + @"SELECT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime], [o].[MainEntityId], [o].[Description], [o].[EndTime], [o].[StartTime] +FROM ( + SELECT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] +) AS [m] +LEFT JOIN [OwnedEntity] AS [o] ON [m].[Id] = [o].[MainEntityId]"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_subquery(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .TemporalAsOf(date) + .Distinct() + .OrderByDescending(x => x.Id) + .Take(3); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"@__p_0='3' + +SELECT [t0].[Id], [t0].[Description], [t0].[EndTime], [t0].[StartTime], [o].[MainEntityId], [o].[Description], [o].[EndTime], [o].[StartTime] +FROM ( + SELECT TOP(@__p_0) [t].[Id], [t].[Description], [t].[EndTime], [t].[StartTime] + FROM ( + SELECT DISTINCT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] + ) AS [t] + ORDER BY [t].[Id] DESC +) AS [t0] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [t0].[Id] = [o].[MainEntityId] +ORDER BY [t0].[Id] DESC"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_complex(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities.TemporalAsOf(date) + .Join(context.MainEntities, x => x.Id, x => x.Id, (o, i) => new { o, i }) + .Distinct().OrderByDescending(x => x.o.Id).Take(3) + .Join(context.MainEntities, xx => xx.o.Id, x => x.Id, (o, i) => new { o, i }); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"@__p_0='3' + +SELECT [t0].[Id], [t0].[Description], [t0].[EndTime], [t0].[StartTime], [o].[MainEntityId], [o].[Description], [o].[EndTime], [o].[StartTime], [t0].[Id0], [t0].[Description0], [t0].[EndTime0], [t0].[StartTime0], [o0].[MainEntityId], [o0].[Description], [o0].[EndTime], [o0].[StartTime], [m1].[Id], [m1].[Description], [m1].[EndTime], [m1].[StartTime], [o1].[MainEntityId], [o1].[Description], [o1].[EndTime], [o1].[StartTime] +FROM ( + SELECT TOP(@__p_0) [t].[Id], [t].[Description], [t].[EndTime], [t].[StartTime], [t].[Id0], [t].[Description0], [t].[EndTime0], [t].[StartTime0] + FROM ( + SELECT DISTINCT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime], [m0].[Id] AS [Id0], [m0].[Description] AS [Description0], [m0].[EndTime] AS [EndTime0], [m0].[StartTime] AS [StartTime0] + FROM [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m] + INNER JOIN [MainEntity] AS [m0] ON [m].[Id] = [m0].[Id] + ) AS [t] + ORDER BY [t].[Id] DESC +) AS [t0] +INNER JOIN [MainEntity] AS [m1] ON [t0].[Id] = [m1].[Id] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o] ON [t0].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntity] AS [o0] ON [t0].[Id0] = [o0].[MainEntityId] +LEFT JOIN [OwnedEntity] AS [o1] ON [m1].[Id] = [o1].[MainEntityId] +ORDER BY [t0].[Id] DESC"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_complex_with_nontrivial_alias(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var date = new DateTime(2000, 1, 1); + + var query = context.MainEntities + .Join(context.MainEntities.TemporalAsOf(date), x => x.Id, x => x.Id, (o, i) => new { o, i }) + .Distinct().OrderByDescending(x => x.o.Id).Take(3) + .Join(context.MainEntities, xx => xx.o.Id, x => x.Id, (o, i) => new { o, i }); + + var _ = async ? await query.ToListAsync() : query.ToList(); + } + + AssertSql( + @"@__p_0='3' + +SELECT [t0].[Id], [t0].[Description], [t0].[EndTime], [t0].[StartTime], [o].[MainEntityId], [o].[Description], [o].[EndTime], [o].[StartTime], [t0].[Id0], [t0].[Description0], [t0].[EndTime0], [t0].[StartTime0], [o0].[MainEntityId], [o0].[Description], [o0].[EndTime], [o0].[StartTime], [m1].[Id], [m1].[Description], [m1].[EndTime], [m1].[StartTime], [o1].[MainEntityId], [o1].[Description], [o1].[EndTime], [o1].[StartTime] +FROM ( + SELECT TOP(@__p_0) [t].[Id], [t].[Description], [t].[EndTime], [t].[StartTime], [t].[Id0], [t].[Description0], [t].[EndTime0], [t].[StartTime0] + FROM ( + SELECT DISTINCT [m].[Id], [m].[Description], [m].[EndTime], [m].[StartTime], [m0].[Id] AS [Id0], [m0].[Description] AS [Description0], [m0].[EndTime] AS [EndTime0], [m0].[StartTime] AS [StartTime0] + FROM [MainEntity] AS [m] + INNER JOIN [MainEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [m0] ON [m].[Id] = [m0].[Id] + ) AS [t] + ORDER BY [t].[Id] DESC +) AS [t0] +INNER JOIN [MainEntity] AS [m1] ON [t0].[Id] = [m1].[Id] +LEFT JOIN [OwnedEntity] AS [o] ON [t0].[Id] = [o].[MainEntityId] +LEFT JOIN [OwnedEntity] FOR SYSTEM_TIME AS OF '2000-01-01T00:00:00.0000000' AS [o0] ON [t0].[Id0] = [o0].[MainEntityId] +LEFT JOIN [OwnedEntity] AS [o1] ON [m1].[Id] = [o1].[MainEntityId] +ORDER BY [t0].[Id] DESC"); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public virtual async Task Temporal_owned_range_operation_negative(bool async) + { + var contextFactory = await InitializeAsync(); + using (var context = contextFactory.CreateContext()) + { + var message = async + ? (await Assert.ThrowsAsync( + () => context.MainEntities.TemporalAll().ToListAsync())).Message + : Assert.Throws(() => context.MainEntities.TemporalAll().ToList()).Message; + + Assert.Equal( + SqlServerStrings.TemporalOwnedTypeMappedToDifferentTableOnlySupportedForAsOf("AsOf"), + message); + } + } + + public class MainEntity + { + public int Id { get; set; } + public string Description { get; set; } + public OwnedEntity OwnedEntity { get; set; } + } + + public class OwnedEntity + { + public string Description { get; set; } + } + + public class MyContext26451 : DbContext + { + public MyContext26451(DbContextOptions options) + : base(options) + { + } + + public DbSet MainEntities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("MainEntity", tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("StartTime"); + ttb.HasPeriodEnd("EndTime"); + ttb.UseHistoryTable("ConfHistory"); + })); + modelBuilder.Entity().Property(me => me.Id).UseIdentityColumn(); + modelBuilder.Entity().OwnsOne(me => me.OwnedEntity).WithOwner(); + modelBuilder.Entity().OwnsOne(me => me.OwnedEntity, oe => + { + oe.ToTable("OwnedEntity", tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("StartTime"); + ttb.HasPeriodEnd("EndTime"); + ttb.UseHistoryTable("OwnedEntityHistory"); + })); + }); + } + } + } +}