diff --git a/src/EFCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs new file mode 100644 index 00000000000..dd2a9b249a9 --- /dev/null +++ b/src/EFCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs @@ -0,0 +1,211 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Sql Server database specific extension methods for LINQ queries. + /// + public static class SqlServerQueryableExtensions + { + /// + /// + /// Applies temporal 'AsOf' operation on the given DbSet, which only returns elements that were present in the database at a given point in time. + /// + /// + /// Temporal queries are always set as 'NoTracking'. + /// + /// + /// Source DbSet on which the temporal operation is applied. + /// representing a point in time for which the results should be returned. + /// An representing the entities at a given point in time. + public static IQueryable TemporalAsOf( + this DbSet source, + DateTime pointInTime) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + var queryableSource = (IQueryable)source; + + return queryableSource.Provider.CreateQuery( + GenerateTemporalAsOfQueryRoot( + queryableSource, + pointInTime)).AsNoTracking(); + } + + /// + /// + /// Applies temporal 'FromTo' operation on the given DbSet, which only returns elements that were present in the database between two points in time. + /// + /// + /// Elements that were created at the starting point as well as elements that were removed at the end point are not included in the results. + /// + /// + /// All versions of entities in that were present within the time range are returned, so it is possible to return multiple entities with the same key. + /// + /// + /// Temporal queries are always set as 'NoTracking'. + /// + /// + /// Source DbSet on which the temporal operation is applied. + /// Point in time representing the start of the period for which results should be returned. + /// Point in time representing the end of the period for which results should be returned. + /// An representing the entities present in a given time range. + public static IQueryable TemporalFromTo( + this DbSet source, + DateTime from, + DateTime to) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + var queryableSource = (IQueryable)source; + + return queryableSource.Provider.CreateQuery( + GenerateRangeTemporalQueryRoot( + queryableSource, + from, + to, + TemporalOperationType.FromTo)).AsNoTracking(); + } + + /// + /// + /// Applies temporal 'Between' operation on the given DbSet, which only returns elements that were present in the database between two points in time. + /// + /// + /// Elements that were created at the starting point are not included in the results, however elements that were removed at the end point are included in the results. + /// + /// + /// All versions of entities in that were present within the time range are returned, so it is possible to return multiple entities with the same key. + /// + /// + /// Temporal queries are always set as 'NoTracking'. + /// + /// + /// Source DbSet on which the temporal operation is applied. + /// Point in time representing the start of the period for which results should be returned. + /// Point in time representing the end of the period for which results should be returned. + /// An representing the entities present in a given time range. + public static IQueryable TemporalBetween( + this IQueryable source, + DateTime from, + DateTime to) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + var queryableSource = (IQueryable)source; + + return queryableSource.Provider.CreateQuery( + GenerateRangeTemporalQueryRoot( + queryableSource, + from, + to, + TemporalOperationType.Between)).AsNoTracking(); + } + + /// + /// + /// Applies temporal 'ContainedIn' operation on the given DbSet, which only returns elements that were present in the database between two points in time. + /// + /// + /// Elements that were created at the starting point as well as elements that were removed at the end point are included in the results. + /// + /// + /// All versions of entities in that were present within the time range are returned, so it is possible to return multiple entities with the same key. + /// + /// + /// Temporal queries are always set as 'NoTracking'. + /// + /// + /// Source DbSet on which the temporal operation is applied. + /// Point in time representing the start of the period for which results should be returned. + /// Point in time representing the end of the period for which results should be returned. + /// An representing the entities present in a given time range. + public static IQueryable TemporalContainedIn( + this DbSet source, + DateTime from, + DateTime to) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + var queryableSource = (IQueryable)source; + + return queryableSource.Provider.CreateQuery( + GenerateRangeTemporalQueryRoot( + queryableSource, + from, + to, + TemporalOperationType.ContainedIn)).AsNoTracking(); + } + + /// + /// + /// Applies temporal 'All' operation on the given DbSet, which returns all historical versions of the entities as well as their current state. + /// + /// + /// Temporal queries are always set as 'NoTracking'. + /// + /// + /// Source DbSet on which the temporal operation is applied. + /// An representing the entities and their historical versions. + public static IQueryable TemporalAll( + this DbSet source) + where TEntity : class + { + Check.NotNull(source, nameof(source)); + + var queryableSource = (IQueryable)source; + var queryRootExpression = (QueryRootExpression)queryableSource.Expression; + var entityType = queryRootExpression.EntityType; + + var temporalQueryRootExpression = new TemporalAllQueryRootExpression( + queryRootExpression.QueryProvider!, + entityType); + + return queryableSource.Provider.CreateQuery(temporalQueryRootExpression) + .AsNoTracking(); + } + + private static Expression GenerateTemporalAsOfQueryRoot( + IQueryable source, + DateTime pointInTime) + { + var queryRootExpression = (QueryRootExpression)source.Expression; + var entityType = queryRootExpression.EntityType; + + return new TemporalAsOfQueryRootExpression( + queryRootExpression.QueryProvider!, + entityType, + pointInTime: pointInTime); + } + + private static Expression GenerateRangeTemporalQueryRoot( + IQueryable source, + DateTime from, + DateTime to, + TemporalOperationType temporalOperationType) + { + var queryRootExpression = (QueryRootExpression)source.Expression; + var entityType = queryRootExpression.EntityType; + + return new TemporalRangeQueryRootExpression( + queryRootExpression.QueryProvider!, + entityType, + from: from, + to: to, + temporalOperationType: temporalOperationType); + } + } +} diff --git a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs index 2585a87b882..8b8981031f5 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerServiceCollectionExtensions.cs @@ -79,6 +79,8 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec .TryAdd() .TryAdd() .TryAdd() + .TryAdd() + .TryAdd() .TryAddProviderSpecificServices( b => b .TryAddSingleton() diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs index c7729b91c62..f025c83bdeb 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs @@ -64,9 +64,10 @@ public override ConventionSet CreateConventionSet() ReplaceConvention( conventionSet.EntityTypeAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention); + var sqlServerTemporalConvention = new SqlServerTemporalConvention(); ConventionSet.AddBefore( conventionSet.EntityTypeAnnotationChangedConventions, - new SqlServerTemporalConvention(), + sqlServerTemporalConvention, typeof(SqlServerValueGenerationConvention)); ReplaceConvention(conventionSet.EntityTypePrimaryKeyChangedConventions, valueGenerationConvention); @@ -110,6 +111,8 @@ public override ConventionSet CreateConventionSet() conventionSet.ModelFinalizedConventions, (RuntimeModelConvention)new SqlServerRuntimeModelConvention(Dependencies, RelationalDependencies)); + conventionSet.SkipNavigationForeignKeyChangedConventions.Add(sqlServerTemporalConvention); + return conventionSet; } diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs index bd3650a7780..6f81e36a72d 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs @@ -10,7 +10,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions /// /// A convention that manipulates temporal settings for an entity mapped to a temporal table. /// - public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConvention + public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConvention, ISkipNavigationForeignKeyChangedConvention { private const string PeriodStartDefaultName = "PeriodStart"; private const string PeriodEndDefaultName = "PeriodEnd"; @@ -81,5 +81,24 @@ public virtual void ProcessEntityTypeAnnotationChanged( } } } + + /// + public virtual void ProcessSkipNavigationForeignKeyChanged( + IConventionSkipNavigationBuilder skipNavigationBuilder, + IConventionForeignKey? foreignKey, + IConventionForeignKey? oldForeignKey, + IConventionContext context) + { + if (skipNavigationBuilder.Metadata.JoinEntityType is IConventionEntityType joinEntityType + && joinEntityType.HasSharedClrType + && !joinEntityType.IsTemporal() + && joinEntityType.GetConfigurationSource() == ConfigurationSource.Convention + && skipNavigationBuilder.Metadata.DeclaringEntityType.IsTemporal() + && skipNavigationBuilder.Metadata.Inverse is IConventionSkipNavigation inverse + && inverse.DeclaringEntityType.IsTemporal()) + { + joinEntityType.SetIsTemporal(true); + } + } } } diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index b37ba30cfed..dae43f18bcf 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -313,6 +313,30 @@ public static string TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue(ob GetString("TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue", nameof(entityType), nameof(propertyName)), entityType, propertyName); + /// + /// Temporal query is trying to use navigation to an entity '{entityType}' which itself doesn't map to temporal table. Either map the entity to temporal table or use join manually to access it. + /// + public static string TemporalNavigationExpansionBetweenTemporalAndNonTemporal(object? entityType) + => string.Format( + GetString("TemporalNavigationExpansionBetweenTemporalAndNonTemporal", nameof(entityType)), + entityType); + + /// + /// Navigation expansion is only supported for '{operationName}' temporal operation. For other operations use join manually. + /// + public static string TemporalNavigationExpansionOnlySupportedForAsOf(object? operationName) + => string.Format( + GetString("TemporalNavigationExpansionOnlySupportedForAsOf", nameof(operationName)), + operationName); + + /// + /// Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match. + /// + public static string TemporalSetOperationOnMismatchedSources(object? entityType) + => string.Format( + GetString("TemporalSetOperationOnMismatchedSources", nameof(entityType)), + entityType); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 0d791f4e64e..0def370989f 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -319,4 +319,16 @@ Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified. + + Temporal query is trying to use navigation to an entity '{entityType}' which itself doesn't map to temporal table. Either map the entity to temporal table or use join manually to access it. + + + + Navigation expansion is only supported for '{operationName}' temporal operation. For other operations use join manually. + + + + Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match. + + \ No newline at end of file diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs index d34e6d26fae..709e08f27c1 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerParameterBasedSqlProcessor.cs @@ -53,5 +53,21 @@ public override SelectExpression Optimize( return (SelectExpression)new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory) .Visit(optimizedSelectExpression); } + + /// + /// 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. + /// + protected override SelectExpression ProcessSqlNullability( + SelectExpression selectExpression, + IReadOnlyDictionary parametersValues, out bool canCache) + { + Check.NotNull(selectExpression, nameof(selectExpression)); + Check.NotNull(parametersValues, nameof(parametersValues)); + + return new SqlServerSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(selectExpression, parametersValues, out canCache); + } } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootCreator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootCreator.cs new file mode 100644 index 00000000000..ebfe5c8fad2 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryRootCreator.cs @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 class SqlServerQueryRootCreator : QueryRootCreator + { + /// + /// 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 SqlServerQueryRootCreator(QueryRootCreatorDependencies dependencies) + : base(dependencies) + { + } + + /// + /// 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 override QueryRootExpression CreateQueryRoot(IEntityType entityType, QueryRootExpression? source) + { + if (source is TemporalAsOfQueryRootExpression + || source is TemporalRangeQueryRootExpression + || source is TemporalAllQueryRootExpression) + { + if (!entityType.GetRootType().IsTemporal()) + { + throw new InvalidOperationException(SqlServerStrings.TemporalNavigationExpansionBetweenTemporalAndNonTemporal(entityType.DisplayName())); + } + + if (source is TemporalAsOfQueryRootExpression asOf) + { + return source.QueryProvider != null + ? new TemporalAsOfQueryRootExpression(source.QueryProvider, entityType, asOf.PointInTime) + : new TemporalAsOfQueryRootExpression(entityType, asOf.PointInTime); + } + + throw new InvalidOperationException(SqlServerStrings.TemporalNavigationExpansionOnlySupportedForAsOf(nameof(TemporalOperationType.AsOf))); + } + + return base.CreateQueryRoot(entityType, source); + } + + /// + /// 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 override bool AreCompatible(QueryRootExpression? first, QueryRootExpression? second) + { + if (!base.AreCompatible(first, second)) + { + return false; + } + + var firstTemporal = first is TemporalAsOfQueryRootExpression + || first is TemporalRangeQueryRootExpression + || first is TemporalAllQueryRootExpression; + + var secondTemporal = second is TemporalAsOfQueryRootExpression + || second is TemporalRangeQueryRootExpression + || second is TemporalAllQueryRootExpression; + + if (firstTemporal && secondTemporal) + { + if (first is TemporalAsOfQueryRootExpression firstAsOf + && second is TemporalAsOfQueryRootExpression secondAsOf + && firstAsOf.PointInTime == secondAsOf.PointInTime) + { + return true; + } + + if (first is TemporalAllQueryRootExpression + && second is TemporalAllQueryRootExpression) + { + return true; + } + + if (first is TemporalRangeQueryRootExpression firstRange + && second is TemporalRangeQueryRootExpression secondRange + && firstRange.From == secondRange.From + && firstRange.To == secondRange.To) + { + return true; + } + } + + if (firstTemporal || secondTemporal) + { + var entityType = first?.EntityType ?? second?.EntityType; + + throw new InvalidOperationException(SqlServerStrings.TemporalSetOperationOnMismatchedSources(entityType!.DisplayName())); + } + + return true; + } + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 07ab13a2f3c..6bafa0d2fcd 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -1,8 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.SqlServer.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal @@ -97,5 +101,70 @@ protected override void GenerateLimitOffset(SelectExpression selectExpression) } } } + + /// + /// 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. + /// + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is TemporalTableExpression temporalTableExpression) + { + Sql + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(temporalTableExpression.Name, temporalTableExpression.Schema)) + .Append(" FOR SYSTEM_TIME "); + + switch (temporalTableExpression.TemporalOperationType) + { + case TemporalOperationType.AsOf: + Sql + .Append("AS OF ") + .Append(Sql.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(temporalTableExpression.PointInTime)); + break; + + case TemporalOperationType.FromTo: + Sql + .Append("FROM ") + .Append(Sql.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(temporalTableExpression.From)) + .Append(" TO ") + .Append(Sql.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(temporalTableExpression.To)); + break; + + case TemporalOperationType.Between: + Sql + .Append("BETWEEN ") + .Append(Sql.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(temporalTableExpression.From)) + .Append(" AND ") + .Append(Sql.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(temporalTableExpression.To)); + break; + + case TemporalOperationType.ContainedIn: + Sql + .Append("CONTAINED IN (") + .Append(Sql.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(temporalTableExpression.From)) + .Append(", ") + .Append(Sql.TypeMappingSource.GetMapping(typeof(DateTime)).GenerateSqlLiteral(temporalTableExpression.To)) + .Append(")"); + break; + + default: + Sql.Append("ALL"); + break; + } + + if (temporalTableExpression.Alias != null) + { + Sql + .Append(AliasSeparator) + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(temporalTableExpression.Alias)); + } + + return temporalTableExpression; + } + + return base.VisitExtension(extensionExpression); + } } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs new file mode 100644 index 00000000000..8c6ddcdf167 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -0,0 +1,101 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.SqlServer.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 class SqlServerQueryableMethodTranslatingExpressionVisitor : RelationalQueryableMethodTranslatingExpressionVisitor + { + /// + /// 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 SqlServerQueryableMethodTranslatingExpressionVisitor( + QueryableMethodTranslatingExpressionVisitorDependencies dependencies, + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies, + QueryCompilationContext queryCompilationContext) + : base(dependencies, relationalDependencies, queryCompilationContext) + { + } + + /// + /// 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. + /// + protected SqlServerQueryableMethodTranslatingExpressionVisitor( + SqlServerQueryableMethodTranslatingExpressionVisitor parentVisitor) + : base(parentVisitor) + { + } + + /// + /// 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. + /// + protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVisitor() + => new SqlServerQueryableMethodTranslatingExpressionVisitor(this); + + /// + /// 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. + /// + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is TemporalAsOfQueryRootExpression + || extensionExpression is TemporalRangeQueryRootExpression + || extensionExpression is TemporalAllQueryRootExpression) + { + var queryRootExpression = (QueryRootExpression)extensionExpression; + + // sql server model validator will throw if entity is mapped to multiple tables + var table = queryRootExpression.EntityType.GetTableMappings().Single().Table; + var temporalTableExpression = queryRootExpression switch + { + TemporalRangeQueryRootExpression range => new TemporalTableExpression( + table, + range.From, + range.To, + range.TemporalOperationType), + TemporalAsOfQueryRootExpression asOf => new TemporalTableExpression(table, asOf.PointInTime), + // all + _ => new TemporalTableExpression(table), + }; + + var selectExpression = RelationalDependencies.SqlExpressionFactory.Select( + queryRootExpression.EntityType, + temporalTableExpression); + + return new ShapedQueryExpression( + selectExpression, + new RelationalEntityShaperExpression( + queryRootExpression.EntityType, + new ProjectionBindingExpression( + selectExpression, + new ProjectionMember(), + typeof(ValueBuffer)), + false)); + } + + return base.VisitExtension(extensionExpression); + } + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitorFactory.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitorFactory.cs new file mode 100644 index 00000000000..09735491a5a --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitorFactory.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 class SqlServerQueryableMethodTranslatingExpressionVisitorFactory : IQueryableMethodTranslatingExpressionVisitorFactory + { + private readonly QueryableMethodTranslatingExpressionVisitorDependencies _dependencies; + private readonly RelationalQueryableMethodTranslatingExpressionVisitorDependencies _relationalDependencies; + + /// + /// 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 SqlServerQueryableMethodTranslatingExpressionVisitorFactory( + QueryableMethodTranslatingExpressionVisitorDependencies dependencies, + RelationalQueryableMethodTranslatingExpressionVisitorDependencies relationalDependencies) + { + Check.NotNull(dependencies, nameof(dependencies)); + Check.NotNull(relationalDependencies, nameof(relationalDependencies)); + + _dependencies = dependencies; + _relationalDependencies = relationalDependencies; + } + + /// + /// 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 virtual QueryableMethodTranslatingExpressionVisitor Create(QueryCompilationContext queryCompilationContext) + { + Check.NotNull(queryCompilationContext, nameof(queryCompilationContext)); + + return new SqlServerQueryableMethodTranslatingExpressionVisitor(_dependencies, _relationalDependencies, queryCompilationContext); + } + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs new file mode 100644 index 00000000000..1471ae5d459 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.SqlServer.Query.SqlExpressions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 class SqlServerSqlNullabilityProcessor : SqlNullabilityProcessor + { + /// + /// 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 SqlServerSqlNullabilityProcessor(RelationalParameterBasedSqlProcessorDependencies dependencies, bool useRelationalNulls) + : base(dependencies, useRelationalNulls) + { + } + + /// + /// 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. + /// + protected override TableExpressionBase Visit(TableExpressionBase tableExpressionBase) + { + if (tableExpressionBase is TemporalTableExpression temporalTableExpression) + { + return temporalTableExpression; + } + + return base.Visit(tableExpressionBase); + } + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/TemporalAllQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalAllQueryRootExpression.cs new file mode 100644 index 00000000000..f86d24556c4 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/TemporalAllQueryRootExpression.cs @@ -0,0 +1,97 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 class TemporalAllQueryRootExpression : QueryRootExpression + { + /// + /// 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 TemporalAllQueryRootExpression( + IEntityType entityType) + : base(entityType) + { + } + + /// + /// 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 TemporalAllQueryRootExpression( + IAsyncQueryProvider queryProvider, + IEntityType entityType) + : base(queryProvider, entityType) + { + } + + /// + /// 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 override Expression DetachQueryProvider() + => new TemporalAllQueryRootExpression(EntityType); + + /// + /// 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. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + /// + /// 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. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + base.Print(expressionPrinter); + expressionPrinter.Append($".TemporalAll()"); + } + + /// + /// 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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is TemporalAllQueryRootExpression queryRootExpression + && Equals(queryRootExpression)); + + private bool Equals(TemporalAllQueryRootExpression queryRootExpression) + => base.Equals(queryRootExpression); + + /// + /// 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 override int GetHashCode() + => base.GetHashCode(); + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/TemporalAsOfQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalAsOfQueryRootExpression.cs new file mode 100644 index 00000000000..252aa1cbe9a --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/TemporalAsOfQueryRootExpression.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 class TemporalAsOfQueryRootExpression : QueryRootExpression + { + /// + /// 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 TemporalAsOfQueryRootExpression( + IEntityType entityType, + DateTime pointInTime) + : base(entityType) + { + PointInTime = pointInTime; + } + + /// + /// 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 TemporalAsOfQueryRootExpression( + IAsyncQueryProvider queryProvider, + IEntityType entityType, + DateTime pointInTime) + : base(queryProvider, entityType) + { + PointInTime = pointInTime; + } + + /// + /// 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 virtual DateTime PointInTime { get; } + + /// + /// 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 override Expression DetachQueryProvider() + => new TemporalAsOfQueryRootExpression(EntityType, PointInTime); + + /// + /// 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. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + /// + /// 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. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + base.Print(expressionPrinter); + expressionPrinter.Append($".TemporalAsOf({PointInTime})"); + } + + /// + /// 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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is TemporalAsOfQueryRootExpression queryRootExpression + && Equals(queryRootExpression)); + + private bool Equals(TemporalAsOfQueryRootExpression queryRootExpression) + => base.Equals(queryRootExpression) + && Equals(PointInTime, queryRootExpression.PointInTime); + + /// + /// 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 override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(PointInTime); + + return hashCode.ToHashCode(); + } + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/TemporalOperationType.cs b/src/EFCore.SqlServer/Query/Internal/TemporalOperationType.cs new file mode 100644 index 00000000000..8a970d5a0f3 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/TemporalOperationType.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 enum TemporalOperationType + { + /// + /// 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. + /// + AsOf, + + /// + /// 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. + /// + FromTo, + + /// + /// 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. + /// + Between, + + /// + /// 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. + /// + ContainedIn, + + /// + /// 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. + /// + All, + } +} diff --git a/src/EFCore.SqlServer/Query/Internal/TemporalRangeQueryRootExpression.cs b/src/EFCore.SqlServer/Query/Internal/TemporalRangeQueryRootExpression.cs new file mode 100644 index 00000000000..4fc01bfb9f6 --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/TemporalRangeQueryRootExpression.cs @@ -0,0 +1,158 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal +{ + /// + /// 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 class TemporalRangeQueryRootExpression : QueryRootExpression + { + /// + /// 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 TemporalRangeQueryRootExpression( + IEntityType entityType, + DateTime from, + DateTime to, + TemporalOperationType temporalOperationType) + : base(entityType) + { + From = from; + To = to; + TemporalOperationType = temporalOperationType; + } + + /// + /// 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 TemporalRangeQueryRootExpression( + IAsyncQueryProvider queryProvider, + IEntityType entityType, + DateTime from, + DateTime to, + TemporalOperationType temporalOperationType) + : base(queryProvider, entityType) + { + From = from; + To = to; + TemporalOperationType = temporalOperationType; + } + + /// + /// 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 virtual DateTime From { get; } + + /// + /// 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 virtual DateTime To { get; } + + /// + /// 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 virtual TemporalOperationType TemporalOperationType { get; } + + /// + /// 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 override Expression DetachQueryProvider() + => new TemporalRangeQueryRootExpression(EntityType, From, To, TemporalOperationType); + + /// + /// 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. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + /// + /// 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. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + base.Print(expressionPrinter); + switch (TemporalOperationType) + { + case TemporalOperationType.FromTo: + expressionPrinter.Append($".TemporalFromTo({From}, {To})"); + break; + + case TemporalOperationType.Between: + expressionPrinter.Append($".TemporalBetween({From}, {To})"); + break; + + default: + expressionPrinter.Append($".TemporalContainedIn({From}, {To})"); + break; + } + } + + /// + /// 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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is TemporalRangeQueryRootExpression queryRootExpression + && Equals(queryRootExpression)); + + private bool Equals(TemporalRangeQueryRootExpression queryRootExpression) + => base.Equals(queryRootExpression) + && Equals(From, queryRootExpression.From) + && Equals(To, queryRootExpression.To) + && Equals(TemporalOperationType, queryRootExpression.TemporalOperationType); + + /// + /// 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 override int GetHashCode() + { + var hashCode = new HashCode(); + hashCode.Add(base.GetHashCode()); + hashCode.Add(From); + hashCode.Add(To); + hashCode.Add(TemporalOperationType); + + return hashCode.ToHashCode(); + } + } +} diff --git a/src/EFCore.SqlServer/Query/SqlExpressions/TemporalTableExpression.cs b/src/EFCore.SqlServer/Query/SqlExpressions/TemporalTableExpression.cs new file mode 100644 index 00000000000..0120b9be29e --- /dev/null +++ b/src/EFCore.SqlServer/Query/SqlExpressions/TemporalTableExpression.cs @@ -0,0 +1,173 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.SqlExpressions +{ + /// + /// Fill in later + /// + public class TemporalTableExpression : TableExpressionBase, ICloneable + { + /// + /// Fill in later + /// + public TemporalTableExpression(ITableBase table) + : base(table.Name.Substring(0, 1).ToLowerInvariant()) + { + Name = table.Name; + Schema = table.Schema; + TemporalOperationType = TemporalOperationType.All; + } + + /// + /// Fill in later + /// + public TemporalTableExpression(ITableBase table, DateTime pointInTime) + : base(table.Name.Substring(0, 1).ToLowerInvariant()) + { + Name = table.Name; + Schema = table.Schema; + PointInTime = pointInTime; + TemporalOperationType = TemporalOperationType.AsOf; + } + + /// + /// Fill in later + /// + public TemporalTableExpression(ITableBase table, DateTime from, DateTime to, TemporalOperationType temporalOperationType) + : base(table.Name.Substring(0, 1).ToLowerInvariant()) + { + Name = table.Name; + Schema = table.Schema; + From = from; + To = to; + TemporalOperationType = temporalOperationType; + } + + private TemporalTableExpression( + string name, + string? schema, + string? alias, + DateTime? pointInTime, + DateTime? from, + DateTime? to, + TemporalOperationType temporalOperationType) + : base(alias) + { + Name = name; + Schema = schema; + PointInTime = pointInTime; + From = from; + To = to; + TemporalOperationType = temporalOperationType; + } + + /// + /// Fill in later + /// + public virtual string? Schema { get; } + + /// + /// Fill in later + /// + public virtual string Name { get; } + + /// + /// Fill in later + /// + public virtual DateTime? PointInTime { get; } + + /// + /// Fill in later + /// + public virtual DateTime? From { get; } + + /// + /// Fill in later + /// + public virtual DateTime? To { get; } + + /// + /// Fill in later + /// + public virtual TemporalOperationType TemporalOperationType { get; } + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + if (!string.IsNullOrEmpty(Schema)) + { + expressionPrinter.Append(Schema).Append("."); + } + + expressionPrinter + .Append(Name) + .Append(" FOR SYSTEM_TIME "); + + switch (TemporalOperationType) + { + case TemporalOperationType.AsOf: + expressionPrinter + .Append("AS OF ") + .Append(PointInTime.ToString()!); + break; + + case TemporalOperationType.FromTo: + expressionPrinter + .Append("FROM ") + .Append(From.ToString()!) + .Append(" TO ") + .Append(To.ToString()!); + break; + + case TemporalOperationType.Between: + expressionPrinter + .Append("BETWEEN ") + .Append(From.ToString()!) + .Append(" AND ") + .Append(To.ToString()!); + break; + + case TemporalOperationType.ContainedIn: + expressionPrinter + .Append("CONTAINED IN (") + .Append(From.ToString()!) + .Append(", ") + .Append(To.ToString()!) + .Append(")"); + break; + + default: + // TemporalOperationType.All + expressionPrinter + .Append("ALL"); + break; + + } + + if (Alias != null) + { + expressionPrinter.Append(" AS ").Append(Alias); + } + } + + /// + public override bool Equals(object? obj) + // This should be reference equal only. + => obj != null && ReferenceEquals(this, obj); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Name, Schema, PointInTime, From, To, TemporalOperationType); + + /// + public virtual object Clone() + => new TemporalTableExpression(Name, Schema, Alias, PointInTime, From, To, TemporalOperationType); + } +} diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index 727afefe8f2..a0d30b50abb 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -98,6 +98,7 @@ public static readonly IDictionary CoreServices { typeof(IQueryableMethodTranslatingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IQueryTranslationPostprocessorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IShapedQueryCompilingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IQueryRootCreator), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IProviderConventionSetBuilder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IConventionSetBuilder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IDiagnosticsLogger<>), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -272,6 +273,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); TryAdd( p => p.GetService()?.FindExtension()?.DbContextLogger @@ -299,6 +301,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencySingleton() .AddDependencySingleton() .AddDependencySingleton() + .AddDependencySingleton() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 2db61657713..3c0b4ae9caa 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2542,6 +2542,12 @@ public static string ServiceProviderConfigRemoved(object? key) public static string SetOperationWithDifferentIncludesInOperands => GetString("SetOperationWithDifferentIncludesInOperands"); + /// + /// Incompatible sources used for set operation. + /// + public static string IncompatibleSourcesForSetOperation + => GetString("IncompatibleSourcesForSetOperation"); + /// /// The entity type '{entityType}' is in shadow state. A valid model requires all entity types to have a corresponding CLR type. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 7c6482d9d02..242ba9aacb1 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1408,6 +1408,9 @@ Unable to translate set operation since both operands have different 'Include' operations. Consider having same 'Include' applied on both sides. + + Incompatible sources used for set operation. + The entity type '{entityType}' is in shadow state. A valid model requires all entity types to have a corresponding CLR type. Obsolete diff --git a/src/EFCore/Query/IQueryRootCreator.cs b/src/EFCore/Query/IQueryRootCreator.cs new file mode 100644 index 00000000000..3fb2dc87ec1 --- /dev/null +++ b/src/EFCore/Query/IQueryRootCreator.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + /// Service which can create a new given the entity type and source expression. + /// + public interface IQueryRootCreator + { + /// + /// Creates a new . + /// + /// Entity type of the new . + /// Source expression. + QueryRootExpression CreateQueryRoot(IEntityType entityType, QueryRootExpression? source); + + /// + /// Checks whether two query roots are compatible for a set operation to combine them. + /// + /// The first query root. + /// The second query root. + bool AreCompatible(QueryRootExpression? first, QueryRootExpression? second); + } +} diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index 773c1bac20f..79efaf31807 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -28,13 +28,16 @@ private static readonly MethodInfo _objectEqualsMethodInfo private readonly NavigationExpandingExpressionVisitor _navigationExpandingExpressionVisitor; private readonly NavigationExpansionExpression _source; + private readonly IQueryRootCreator _queryRootCreator; public ExpandingExpressionVisitor( NavigationExpandingExpressionVisitor navigationExpandingExpressionVisitor, - NavigationExpansionExpression source) + NavigationExpansionExpression source, + IQueryRootCreator queryRootCreator) { _navigationExpandingExpressionVisitor = navigationExpandingExpressionVisitor; _source = source; + _queryRootCreator = queryRootCreator; Model = navigationExpandingExpressionVisitor._queryCompilationContext.Model; } @@ -43,7 +46,7 @@ public Expression Expand(Expression expression, bool applyIncludes = false) expression = Visit(expression); if (applyIncludes) { - expression = new IncludeExpandingExpressionVisitor(_navigationExpandingExpressionVisitor, _source) + expression = new IncludeExpandingExpressionVisitor(_navigationExpandingExpressionVisitor, _source, _queryRootCreator) .Visit(expression); } @@ -156,7 +159,7 @@ protected Expression ExpandNavigation( return ownedExpansion; } - var ownedEntityReference = new EntityReference(targetType); + var ownedEntityReference = new EntityReference(targetType, entityReference.QueryRootExpression); _navigationExpandingExpressionVisitor.PopulateEagerLoadedNavigations(ownedEntityReference.IncludePaths); ownedEntityReference.MarkAsOptional(); if (entityReference.IncludePaths.TryGetValue(navigation, out var includePath)) @@ -225,7 +228,9 @@ protected Expression ExpandSkipNavigation( { // Second psuedo-navigation is a reference var secondTargetType = navigation.TargetEntityType; - var innerQueryable = new QueryRootExpression(secondTargetType); + // we can use the entity reference here. If the join entity wasn't temporal, + // the query root creator would have thrown the exception when it was being created + var innerQueryable = _queryRootCreator.CreateQueryRoot(secondTargetType, entityReference.QueryRootExpression); var innerSource = (NavigationExpansionExpression)_navigationExpandingExpressionVisitor.Visit(innerQueryable); if (includeTree != null) @@ -273,7 +278,7 @@ protected Expression ExpandSkipNavigation( { // Second psuedo-navigation is a collection var secondTargetType = navigation.TargetEntityType; - var innerQueryable = new QueryRootExpression(secondTargetType); + var innerQueryable = _queryRootCreator.CreateQueryRoot(secondTargetType, entityReference.QueryRootExpression); var innerSource = (NavigationExpansionExpression)_navigationExpandingExpressionVisitor.Visit(innerQueryable); if (includeTree != null) @@ -342,7 +347,7 @@ private Expression ExpandForeignKey( Check.DebugAssert(!targetType.IsOwned(), "Owned entity expanding foreign key."); - var innerQueryable = new QueryRootExpression(targetType); + var innerQueryable = _queryRootCreator.CreateQueryRoot(targetType, entityReference.QueryRootExpression); var innerSource = (NavigationExpansionExpression)_navigationExpandingExpressionVisitor.Visit(innerQueryable); // We detect and copy over include for navigation being expanded automatically @@ -500,8 +505,9 @@ private sealed class IncludeExpandingExpressionVisitor : ExpandingExpressionVisi public IncludeExpandingExpressionVisitor( NavigationExpandingExpressionVisitor navigationExpandingExpressionVisitor, - NavigationExpansionExpression source) - : base(navigationExpandingExpressionVisitor, source) + NavigationExpansionExpression source, + IQueryRootCreator queryRootCreator) + : base(navigationExpandingExpressionVisitor, source, queryRootCreator) { _logger = navigationExpandingExpressionVisitor._queryCompilationContext.Logger; _queryStateManager = navigationExpandingExpressionVisitor._queryCompilationContext.QueryTrackingBehavior @@ -911,12 +917,15 @@ private sealed class PendingSelectorExpandingExpressionVisitor : ExpressionVisit { private readonly NavigationExpandingExpressionVisitor _visitor; private readonly bool _applyIncludes; + private readonly IQueryRootCreator _queryRootCreator; public PendingSelectorExpandingExpressionVisitor( NavigationExpandingExpressionVisitor visitor, + IQueryRootCreator queryRootCreator, bool applyIncludes = false) { _visitor = visitor; + _queryRootCreator = queryRootCreator; _applyIncludes = applyIncludes; } @@ -927,7 +936,7 @@ public PendingSelectorExpandingExpressionVisitor( { _visitor.ApplyPendingOrderings(navigationExpansionExpression); - var pendingSelector = new ExpandingExpressionVisitor(_visitor, navigationExpansionExpression) + var pendingSelector = new ExpandingExpressionVisitor(_visitor, navigationExpansionExpression, _queryRootCreator) .Expand(navigationExpansionExpression.PendingSelector, _applyIncludes); pendingSelector = _visitor._subqueryMemberPushdownExpressionVisitor.Visit(pendingSelector); pendingSelector = _visitor.Visit(pendingSelector); diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index e64e566041f..aadffac2e99 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -15,10 +15,11 @@ public partial class NavigationExpandingExpressionVisitor { private sealed class EntityReference : Expression, IPrintableExpression { - public EntityReference(IEntityType entityType) + public EntityReference(IEntityType entityType, QueryRootExpression? queryRootExpression) { EntityType = entityType; IncludePaths = new IncludeTreeNode(entityType, this, setLoaded: true); + QueryRootExpression = queryRootExpression; } public IEntityType EntityType { get; } @@ -29,6 +30,7 @@ public EntityReference(IEntityType entityType) public bool IsOptional { get; private set; } public IncludeTreeNode IncludePaths { get; private set; } public IncludeTreeNode? LastIncludeTreeNode { get; private set; } + public QueryRootExpression? QueryRootExpression { get; private set; } public override ExpressionType NodeType => ExpressionType.Extension; @@ -45,7 +47,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) public EntityReference Snapshot() { - var result = new EntityReference(EntityType) { IsOptional = IsOptional }; + var result = new EntityReference(EntityType, QueryRootExpression) { IsOptional = IsOptional }; result.IncludePaths = IncludePaths.Snapshot(result); return result; diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index a31267774de..e5dbab7179b 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -10,7 +10,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query.Internal @@ -58,6 +57,7 @@ private static readonly PropertyInfo _queryContextContextPropertyInfo private readonly RemoveRedundantNavigationComparisonExpressionVisitor _removeRedundantNavigationComparisonExpressionVisitor; private readonly HashSet _parameterNames = new(); private readonly ParameterExtractingExpressionVisitor _parameterExtractingExpressionVisitor; + private readonly IQueryRootCreator _queryRootCreator; private readonly HashSet _nonCyclicAutoIncludeEntityTypes; private readonly Dictionary _parameterizedQueryFilterPredicateCache @@ -74,11 +74,13 @@ private readonly Dictionary _parameterizedQueryFi public NavigationExpandingExpressionVisitor( QueryTranslationPreprocessor queryTranslationPreprocessor, QueryCompilationContext queryCompilationContext, - IEvaluatableExpressionFilter evaluatableExpressionFilter) + IEvaluatableExpressionFilter evaluatableExpressionFilter, + IQueryRootCreator queryRootCreator) { _queryTranslationPreprocessor = queryTranslationPreprocessor; _queryCompilationContext = queryCompilationContext; - _pendingSelectorExpandingExpressionVisitor = new PendingSelectorExpandingExpressionVisitor(this); + _queryRootCreator = queryRootCreator; + _pendingSelectorExpandingExpressionVisitor = new PendingSelectorExpandingExpressionVisitor(this, queryRootCreator); _subqueryMemberPushdownExpressionVisitor = new SubqueryMemberPushdownExpressionVisitor(queryCompilationContext.Model); _nullCheckRemovingExpressionVisitor = new NullCheckRemovingExpressionVisitor(); _reducingExpressionVisitor = new ReducingExpressionVisitor(); @@ -108,7 +110,7 @@ public NavigationExpandingExpressionVisitor( public virtual Expression Expand(Expression query) { var result = Visit(query); - result = new PendingSelectorExpandingExpressionVisitor(this, applyIncludes: true).Visit(result); + result = new PendingSelectorExpandingExpressionVisitor(this, _queryRootCreator, applyIncludes: true).Visit(result); result = Reduce(result); var dbContextOnQueryContextPropertyAccess = @@ -169,6 +171,7 @@ protected override Expression VisitExtension(Expression extensionExpression) processedDefiningQueryBody = Visit(processedDefiningQueryBody); processedDefiningQueryBody = _pendingSelectorExpandingExpressionVisitor.Visit(processedDefiningQueryBody); processedDefiningQueryBody = Reduce(processedDefiningQueryBody); + navigationExpansionExpression = CreateNavigationExpansionExpression(processedDefiningQueryBody, entityType); } else @@ -225,7 +228,7 @@ protected override Expression VisitMember(MemberExpression memberExpression) // due to SubqueryMemberPushdown, this may be collection navigation which was not pushed down navigationExpansionExpression = (NavigationExpansionExpression)_pendingSelectorExpandingExpressionVisitor.Visit(navigationExpansionExpression); - var expandedExpression = new ExpandingExpressionVisitor(this, navigationExpansionExpression).Visit(updatedExpression); + var expandedExpression = new ExpandingExpressionVisitor(this, navigationExpansionExpression, _queryRootCreator).Visit(updatedExpression); if (expandedExpression != updatedExpression) { updatedExpression = Visit(expandedExpression); @@ -689,7 +692,7 @@ private NavigationExpansionExpression ProcessCastOfType( && entityReference.EntityType.GetAllBaseTypes().Concat(entityReference.EntityType.GetDerivedTypesInclusive()) .FirstOrDefault(et => et.ClrType == castType) is IEntityType castEntityType) { - var newEntityReference = new EntityReference(castEntityType); + var newEntityReference = new EntityReference(castEntityType, entityReference.QueryRootExpression); if (entityReference.IsOptional) { newEntityReference.MarkAsOptional(); @@ -1086,7 +1089,7 @@ private NavigationExpansionExpression ProcessOrderByThenBy( source.PendingSelector, keySelector.Body); - lambdaBody = new ExpandingExpressionVisitor(this, source).Visit(lambdaBody); + lambdaBody = new ExpandingExpressionVisitor(this, source, _queryRootCreator).Visit(lambdaBody); lambdaBody = _subqueryMemberPushdownExpressionVisitor.Visit(lambdaBody); if (thenBy) @@ -1215,10 +1218,12 @@ private NavigationExpansionExpression ProcessSetOperation( innerSource = (NavigationExpansionExpression)_pendingSelectorExpandingExpressionVisitor.Visit(innerSource); var innerTreeStructure = SnapshotExpression(innerSource.PendingSelector); - if (!CompareIncludes(outerTreeStructure, innerTreeStructure)) - { - throw new InvalidOperationException(CoreStrings.SetOperationWithDifferentIncludesInOperands); - } + ValidateExpressionCompatibility(outerTreeStructure, innerTreeStructure); + + //if (!CompareIncludes(outerTreeStructure, innerTreeStructure)) + //{ + // throw new InvalidOperationException(CoreStrings.SetOperationWithDifferentIncludesInOperands); + //} var outerQueryable = Reduce(outerSource); var innerQueryable = Reduce(innerSource); @@ -1457,12 +1462,20 @@ private Expression ApplyQueryFilter(IEntityType entityType, NavigationExpansionE return navigationExpansionExpression; } - private bool CompareIncludes(Expression outer, Expression inner) + private void ValidateExpressionCompatibility(Expression outer, Expression inner) { if (outer is EntityReference outerEntityReference && inner is EntityReference innerEntityReference) { - return outerEntityReference.IncludePaths.Equals(innerEntityReference.IncludePaths); + if (!outerEntityReference.IncludePaths.Equals(innerEntityReference.IncludePaths)) + { + throw new InvalidOperationException(CoreStrings.SetOperationWithDifferentIncludesInOperands); + } + + if (!_queryRootCreator.AreCompatible(outerEntityReference.QueryRootExpression, innerEntityReference.QueryRootExpression)) + { + throw new InvalidOperationException(CoreStrings.IncompatibleSourcesForSetOperation); + } } if (outer is NewExpression outerNewExpression @@ -1470,23 +1483,21 @@ private bool CompareIncludes(Expression outer, Expression inner) { if (outerNewExpression.Arguments.Count != innerNewExpression.Arguments.Count) { - return false; + throw new InvalidOperationException(CoreStrings.SetOperationWithDifferentIncludesInOperands); } for (var i = 0; i < outerNewExpression.Arguments.Count; i++) { - if (!CompareIncludes(outerNewExpression.Arguments[i], innerNewExpression.Arguments[i])) - { - return false; - } + ValidateExpressionCompatibility(outerNewExpression.Arguments[i], innerNewExpression.Arguments[i]); } - - return true; } - return outer is DefaultExpression outerDefaultExpression + if (outer is DefaultExpression outerDefaultExpression && inner is DefaultExpression innerDefaultExpression - && outerDefaultExpression.Type == innerDefaultExpression.Type; + && outerDefaultExpression.Type != innerDefaultExpression.Type) + { + throw new InvalidOperationException(CoreStrings.SetOperationWithDifferentIncludesInOperands); + } } private MethodCallExpression ConvertToEnumerable(MethodInfo queryableMethod, IEnumerable arguments) @@ -1494,13 +1505,13 @@ private MethodCallExpression ConvertToEnumerable(MethodInfo queryableMethod, IEn var genericTypeArguments = queryableMethod.IsGenericMethod ? queryableMethod.GetGenericArguments() : Array.Empty(); + var enumerableArguments = arguments.Select( arg => arg is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Quote && unaryExpression.Operand is LambdaExpression ? unaryExpression.Operand - : arg) - .ToList(); + : arg).ToList(); if (queryableMethod.Name == nameof(Enumerable.Min)) { @@ -1614,7 +1625,9 @@ private NavigationExpansionExpression CreateNavigationExpansionExpression( Expression sourceExpression, IEntityType entityType) { - var entityReference = new EntityReference(entityType); + // if sourceExpression is not a query root we will throw when trying to construct temporal root expression + // regular queries don't use the query root so they will still be fine + var entityReference = new EntityReference(entityType, sourceExpression as QueryRootExpression); PopulateEagerLoadedNavigations(entityReference.IncludePaths); var currentTree = new NavigationTreeExpression(entityReference); @@ -1637,7 +1650,7 @@ private NavigationExpansionExpression CreateNavigationExpansionExpression( private Expression ExpandNavigationsForSource(NavigationExpansionExpression source, Expression expression) { expression = _removeRedundantNavigationComparisonExpressionVisitor.Visit(expression); - expression = new ExpandingExpressionVisitor(this, source).Visit(expression); + expression = new ExpandingExpressionVisitor(this, source, _queryRootCreator).Visit(expression); expression = _subqueryMemberPushdownExpressionVisitor.Visit(expression); expression = Visit(expression); expression = _pendingSelectorExpandingExpressionVisitor.Visit(expression); diff --git a/src/EFCore/Query/QueryRootCreator.cs b/src/EFCore/Query/QueryRootCreator.cs new file mode 100644 index 00000000000..de667eb9806 --- /dev/null +++ b/src/EFCore/Query/QueryRootCreator.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.Query +{ + /// + public class QueryRootCreator : IQueryRootCreator + { + /// + /// Creates a new instance of the class. + /// + /// Parameter object containing dependencies for this class. + public QueryRootCreator(QueryRootCreatorDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual QueryRootCreatorDependencies Dependencies { get; } + + /// + public virtual QueryRootExpression CreateQueryRoot(IEntityType entityType, QueryRootExpression? source) + => source?.QueryProvider != null + ? new QueryRootExpression(source.QueryProvider, entityType) + : new QueryRootExpression(entityType); + + /// + public virtual bool AreCompatible(QueryRootExpression? first, QueryRootExpression? second) + { + if (first is null && second is null) + { + return true; + } + + if (first is not null && second is not null) + { + return first.EntityType.GetRootType() == second.EntityType.GetRootType(); + } + + return false; + } + } +} diff --git a/src/EFCore/Query/QueryRootCreatorDependencies.cs b/src/EFCore/Query/QueryRootCreatorDependencies.cs new file mode 100644 index 00000000000..fdb2b9d509a --- /dev/null +++ b/src/EFCore/Query/QueryRootCreatorDependencies.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Infrastructure; +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 a single instance + /// is used by many instances. The implementation must be thread-safe. + /// This service cannot depend on services registered as . + /// + /// + public sealed record QueryRootCreatorDependencies + { + /// + /// + /// 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 QueryRootCreatorDependencies() + { + } + } +} diff --git a/src/EFCore/Query/QueryTranslationPreprocessor.cs b/src/EFCore/Query/QueryTranslationPreprocessor.cs index 508f0dd573f..96b2363fd07 100644 --- a/src/EFCore/Query/QueryTranslationPreprocessor.cs +++ b/src/EFCore/Query/QueryTranslationPreprocessor.cs @@ -57,8 +57,12 @@ public virtual Expression Process(Expression query) query = NormalizeQueryableMethod(query); query = new NullCheckRemovingExpressionVisitor().Visit(query); query = new SubqueryMemberPushdownExpressionVisitor(QueryCompilationContext.Model).Visit(query); - query = new NavigationExpandingExpressionVisitor(this, QueryCompilationContext, Dependencies.EvaluatableExpressionFilter) - .Expand(query); + query = new NavigationExpandingExpressionVisitor( + this, + QueryCompilationContext, + Dependencies.EvaluatableExpressionFilter, + Dependencies.QueryRootCreator) + .Expand(query); query = new QueryOptimizingExpressionVisitor().Visit(query); query = new NullCheckRemovingExpressionVisitor().Visit(query); diff --git a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs index 665bcec5bd0..8ed6629582c 100644 --- a/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs +++ b/src/EFCore/Query/QueryTranslationPreprocessorDependencies.cs @@ -52,16 +52,24 @@ public sealed record QueryTranslationPreprocessorDependencies /// [EntityFrameworkInternal] public QueryTranslationPreprocessorDependencies( - IEvaluatableExpressionFilter evaluatableExpressionFilter) + IEvaluatableExpressionFilter evaluatableExpressionFilter, + IQueryRootCreator queryRootCreator) { Check.NotNull(evaluatableExpressionFilter, nameof(evaluatableExpressionFilter)); + Check.NotNull(queryRootCreator, nameof(queryRootCreator)); EvaluatableExpressionFilter = evaluatableExpressionFilter; + QueryRootCreator = queryRootCreator; } /// /// Evaluatable expression filter. /// public IEvaluatableExpressionFilter EvaluatableExpressionFilter { get; init; } + + /// + /// Query root creator. + /// + public IQueryRootCreator QueryRootCreator { get; init; } } } diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index ddee8f46164..1e2edd52416 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -8966,6 +8966,15 @@ public virtual Task Where_TimeOnly_subtract_TimeOnly(bool async) entryCount: 1); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Basic_query_gears(bool async) + { + return AssertQuery( + async, + ss => ss.Set()); + } + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); diff --git a/test/EFCore.Specification.Tests/TestUtilities/ISetSource.cs b/test/EFCore.Specification.Tests/TestUtilities/ISetSource.cs index 5cd5fc971e2..5221c057fa3 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/ISetSource.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/ISetSource.cs @@ -8,6 +8,6 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities public interface ISetSource { IQueryable Set() - where TEntity : class; + where TEntity : class; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs new file mode 100644 index 00000000000..2ffa4ebee27 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsQuerySqlServerTest.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + [SqlServerCondition(SqlServerCondition.SupportsTemporalTablesCascadeDelete)] + public class TemporalComplexNavigationsCollectionsQuerySqlServerTest : ComplexNavigationsCollectionsQueryRelationalTestBase + { + public TemporalComplexNavigationsCollectionsQuerySqlServerTest( + TemporalComplexNavigationsQuerySqlServerFixture fixture, + ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + { + var temporalEntityTypes = new List + { + typeof(Level1), + typeof(Level2), + typeof(Level3), + typeof(Level4), + }; + + var rewriter = new TemporalPointInTimeQueryRewriter(Fixture.ChangesDate, temporalEntityTypes); + + return rewriter.Visit(serverQueryExpression); + } + + public override async Task Multi_level_include_one_to_many_optional_and_one_to_many_optional_produces_valid_sql(bool async) + { + await base.Multi_level_include_one_to_many_optional_and_one_to_many_optional_produces_valid_sql(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [l].[PeriodEnd], [l].[PeriodStart], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id0], [t].[Level2_Optional_Id], [t].[Level2_Required_Id], [t].[Name0], [t].[OneToMany_Optional_Inverse3Id], [t].[OneToMany_Optional_Self_Inverse3Id], [t].[OneToMany_Required_Inverse3Id], [t].[OneToMany_Required_Self_Inverse3Id], [t].[OneToOne_Optional_PK_Inverse3Id], [t].[OneToOne_Optional_Self3Id], [t].[PeriodEnd0], [t].[PeriodStart0] +FROM [LevelOne] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l] +LEFT JOIN ( + SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id], [l0].[PeriodEnd], [l0].[PeriodStart], [l1].[Id] AS [Id0], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name] AS [Name0], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id], [l1].[PeriodEnd] AS [PeriodEnd0], [l1].[PeriodStart] AS [PeriodStart0] + FROM [LevelTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l0] + LEFT JOIN [LevelThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l1] ON [l0].[Id] = [l1].[OneToMany_Optional_Inverse3Id] +) AS [t] ON [l].[Id] = [t].[OneToMany_Optional_Inverse2Id] +ORDER BY [l].[Id], [t].[Id], [t].[Id0]"); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs new file mode 100644 index 00000000000..536c2b14b26 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + // TODO: this now throws due to table splitting validation + //public class TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest : ComplexNavigationsCollectionsSharedQueryTypeRelationalTestBase< + // TemporalComplexNavigationsSharedTypeQuerySqlServerFixture> + //{ + // public TemporalComplexNavigationsCollectionsSharedTypeQuerySqlServerTest( + // TemporalComplexNavigationsSharedTypeQuerySqlServerFixture fixture, + // ITestOutputHelper testOutputHelper) + // : base(fixture) + // { + // Fixture.TestSqlLoggerFactory.Clear(); + // //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + // } + //} +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsQuerySqlServerFixture.cs new file mode 100644 index 00000000000..16be0c6dcdc --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsQuerySqlServerFixture.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class TemporalComplexNavigationsQuerySqlServerFixture : ComplexNavigationsQuerySqlServerFixture + { + protected override string StoreName { get; } = "TemporalComplexNavigations"; + + public DateTime ChangesDate { get; private set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + } + + protected override void Seed(ComplexNavigationsContext context) + { + base.Seed(context); + + ChangesDate = new DateTime(2010, 1, 1); + + var historyTableInfos = new List<(string table, string historyTable)>() + { + ("LevelOne", "Level1History"), + ("LevelTwo", "Level2History"), + ("LevelThree", "Level3History"), + ("LevelFour", "Level4History"), + }; + + // clean up intermittent history since in the Seed method we do fixup in multiple stages + foreach (var historyTableInfo in historyTableInfos) + { + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw($"DELETE FROM [{historyTableInfo.historyTable}]"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[{historyTableInfo.historyTable}]))"); + } + + foreach (var entityOne in context.ChangeTracker.Entries().Where(e => e.Entity is Level1).Select(e => e.Entity)) + { + ((Level1)entityOne).Name = ((Level1)entityOne).Name + "Modified"; + } + + foreach (var entityOne in context.ChangeTracker.Entries().Where(e => e.Entity is Level2).Select(e => e.Entity)) + { + ((Level2)entityOne).Name = ((Level2)entityOne).Name + "Modified"; + } + + foreach (var entityOne in context.ChangeTracker.Entries().Where(e => e.Entity is Level3).Select(e => e.Entity)) + { + ((Level3)entityOne).Name = ((Level3)entityOne).Name + "Modified"; + } + + foreach (var entityOne in context.ChangeTracker.Entries().Where(e => e.Entity is Level4).Select(e => e.Entity)) + { + ((Level4)entityOne).Name = ((Level4)entityOne).Name + "Modified"; + } + + context.SaveChanges(); + + foreach (var historyTableInfo in historyTableInfos) + { + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] DROP PERIOD FOR SYSTEM_TIME"); + + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodStart = '2000-01-01T01:00:00.0000000Z'"); + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodEnd = '2020-07-01T07:00:00.0000000Z'"); + + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd])"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[{historyTableInfo.historyTable}]))"); + } + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsSharedTypeQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsSharedTypeQuerySqlServerFixture.cs new file mode 100644 index 00000000000..46d316a6d2e --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalComplexNavigationsSharedTypeQuerySqlServerFixture.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestModels.ComplexNavigationsModel; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class TemporalComplexNavigationsSharedTypeQuerySqlServerFixture : ComplexNavigationsSharedTypeQuerySqlServerFixture + { + protected override string StoreName { get; } = "TemporalComplexNavigationsOwned"; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerFixture.cs new file mode 100644 index 00000000000..0b0e5f572af --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerFixture.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class TemporalFiltersInheritanceQuerySqlServerFixture : FiltersInheritanceQuerySqlServerFixture + { + protected override string StoreName { get; } = "TemporalFiltersInheritanceQueryTest"; + + public DateTime ChangesDate { get; private set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + } + + protected override void Seed(InheritanceContext context) + { + base.Seed(context); + + ChangesDate = new DateTime(2010, 1, 1); + + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Animal).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Plant).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Country).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Drink).Select(e => e.Entity)); + context.SaveChanges(); + + var historyTableInfos = new List<(string table, string historyTable)>() + { + ("Animals", "AnimalHistory"), + ("Plants", "PlantHistory"), + ("Countries", "CountryHistory"), + ("Drinks", "DrinkHistory"), + }; + + foreach (var historyTableInfo in historyTableInfos) + { + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] DROP PERIOD FOR SYSTEM_TIME"); + + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodStart = '2000-01-01T01:00:00.0000000Z'"); + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodEnd = '2020-07-01T07:00:00.0000000Z'"); + + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd])"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[{historyTableInfo.historyTable}]))"); + } + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs new file mode 100644 index 00000000000..d388c9a2a80 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalFiltersInheritanceQuerySqlServerTest.cs @@ -0,0 +1,152 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.InheritanceModel; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + [SqlServerCondition(SqlServerCondition.SupportsTemporalTablesCascadeDelete)] + public class TemporalFiltersInheritanceQuerySqlServerTest : FiltersInheritanceQueryTestBase + { + public TemporalFiltersInheritanceQuerySqlServerTest(TemporalFiltersInheritanceQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + { + var temporalEntityTypes = new List + { + typeof(Animal), + typeof(Plant), + typeof(Country), + typeof(Drink), + }; + + var rewriter = new TemporalPointInTimeQueryRewriter(Fixture.ChangesDate, temporalEntityTypes); + + return rewriter.Visit(serverQueryExpression); + } + + public override async Task Can_use_of_type_animal(bool async) + { + await base.Can_use_of_type_animal(async); + + AssertSql( + @"SELECT [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE [a].[CountryId] = 1 +ORDER BY [a].[Species]"); + } + + public override async Task Can_use_is_kiwi(bool async) + { + await base.Can_use_is_kiwi(async); + + AssertSql( + @"SELECT [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE ([a].[CountryId] = 1) AND ([a].[Discriminator] = N'Kiwi')"); + } + + public override async Task Can_use_is_kiwi_with_other_predicate(bool async) + { + await base.Can_use_is_kiwi_with_other_predicate(async); + + AssertSql( + @"SELECT [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE ([a].[CountryId] = 1) AND (([a].[Discriminator] = N'Kiwi') AND ([a].[CountryId] = 1))"); } + + public override async Task Can_use_is_kiwi_in_projection(bool async) + { + await base.Can_use_is_kiwi_in_projection(async); + + AssertSql( + @"SELECT CASE + WHEN [a].[Discriminator] = N'Kiwi' THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE [a].[CountryId] = 1"); + } + + public override async Task Can_use_of_type_bird(bool async) + { + await base.Can_use_of_type_bird(async); + + AssertSql( + @"SELECT [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE [a].[CountryId] = 1 +ORDER BY [a].[Species]"); + } + + public override async Task Can_use_of_type_bird_predicate(bool async) + { + await base.Can_use_of_type_bird_predicate(async); + + AssertSql( + @"SELECT [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE ([a].[CountryId] = 1) AND ([a].[CountryId] = 1) +ORDER BY [a].[Species]"); + } + + public override async Task Can_use_of_type_bird_with_projection(bool async) + { + await base.Can_use_of_type_bird_with_projection(async); + + AssertSql( + @"SELECT [a].[EagleId] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE [a].[CountryId] = 1"); + } + + public override async Task Can_use_of_type_bird_first(bool async) + { + await base.Can_use_of_type_bird_first(async); + + AssertSql( + @"SELECT TOP(1) [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[Group], [a].[FoundOn] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE [a].[CountryId] = 1 +ORDER BY [a].[Species]"); + } + + public override async Task Can_use_of_type_kiwi(bool async) + { + await base.Can_use_of_type_kiwi(async); + + AssertSql( + @"SELECT [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[FoundOn] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE ([a].[CountryId] = 1) AND ([a].[Discriminator] = N'Kiwi')"); + } + + public override async Task Can_use_derived_set(bool async) + { + await base.Can_use_derived_set(async); + + AssertSql( + @"SELECT [a].[Species], [a].[CountryId], [a].[Discriminator], [a].[Name], [a].[PeriodEnd], [a].[PeriodStart], [a].[EagleId], [a].[IsFlightless], [a].[Group] +FROM [Animals] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [a] +WHERE ([a].[Discriminator] = N'Eagle') AND ([a].[CountryId] = 1)"); + } + + public override Task Can_use_IgnoreQueryFilters_and_GetDatabaseValues(bool async) + => Task.CompletedTask; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } + +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerFixture.cs new file mode 100644 index 00000000000..0abd45207f9 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerFixture.cs @@ -0,0 +1,96 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class TemporalGearsOfWarQuerySqlServerFixture : GearsOfWarQuerySqlServerFixture + { + protected override string StoreName { get; } = "TemporalGearsOfWarQueryTest"; + + public DateTime ChangesDate { get; private set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + + base.OnModelCreating(modelBuilder, context); + } + + protected override void Seed(GearsOfWarContext context) + { + base.Seed(context); + + ChangesDate = new DateTime(2010, 1, 1); + + //// clean up intermittent history - we do the data fixup in 2 steps (due to cycle) + //// so we want to remove the temporary states, so that further manipulation is easier + context.Database.ExecuteSqlRaw("ALTER TABLE [LocustLeaders] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw("DELETE FROM [LocustLeaderHistory]"); + context.Database.ExecuteSqlRaw("ALTER TABLE [LocustLeaders] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[LocustLeaderHistory]))"); + + context.Database.ExecuteSqlRaw("ALTER TABLE [Missions] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw("DELETE FROM [MissionHistory]"); + context.Database.ExecuteSqlRaw("ALTER TABLE [Missions] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[MissionHistory]))"); + + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is City).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is CogTag).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Gear).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is LocustHighCommand).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Mission).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Squad).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is SquadMission).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Weapon).Select(e => e.Entity)); + context.SaveChanges(); + + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is Faction).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is LocustLeader).Select(e => e.Entity)); + context.SaveChanges(); + + // clean up Faction history + context.Database.ExecuteSqlRaw("ALTER TABLE [Factions] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw("DELETE FROM [FactionHistory] WHERE CommanderName IS NULL"); + context.Database.ExecuteSqlRaw("ALTER TABLE [Factions] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[FactionHistory]))"); + + var historyTableInfos = new List<(string table, string historyTable)>() + { + ("Cities", "CityHistory"), + ("Tags", "CogTagHistory"), + ("Gears", "GearHistory"), + ("LocustHighCommands", "LocustHighCommandHistory"), + ("Missions", "MissionHistory"), + ("Squads", "SquadHistory"), + ("SquadMissions", "SquadMissionHistory"), + ("Weapons", "WeaponHistory"), + + ("LocustLeaders", "LocustLeaderHistory"), + ("Factions", "FactionHistory"), + }; + + foreach (var historyTableInfo in historyTableInfos) + { + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] DROP PERIOD FOR SYSTEM_TIME"); + + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodStart = '2000-01-01T01:00:00.0000000Z'"); + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodEnd = '2020-07-01T07:00:00.0000000Z'"); + + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd])"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[{historyTableInfo.historyTable}]))"); + } + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs new file mode 100644 index 00000000000..bc2f7afc73f --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -0,0 +1,391 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; +using Microsoft.EntityFrameworkCore.TestModels.GearsOfWarModel; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + [SqlServerCondition(SqlServerCondition.SupportsTemporalTablesCascadeDelete)] + public class TemporalGearsOfWarQuerySqlServerTest : GearsOfWarQueryRelationalTestBase + { +#pragma warning disable IDE0060 // Remove unused parameter + public TemporalGearsOfWarQuerySqlServerTest(TemporalGearsOfWarQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) +#pragma warning restore IDE0060 // Remove unused parameter + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + { + var temporalEntityTypes = new List + { + typeof(City), + typeof(CogTag), + typeof(Faction), + typeof(LocustHorde), + typeof(Gear), + typeof(Officer), + typeof(LocustLeader), + typeof(LocustCommander), + typeof(LocustHighCommand), + typeof(Mission), + typeof(Squad), + typeof(SquadMission), + typeof(Weapon), + }; + + var rewriter = new TemporalPointInTimeQueryRewriter(Fixture.ChangesDate, temporalEntityTypes); + + return rewriter.Visit(serverQueryExpression); + } + + public override Task Include_where_list_contains_navigation(bool async) + => Task.CompletedTask; + + public override Task Include_where_list_contains_navigation2(bool async) + => Task.CompletedTask; + + public override Task Navigation_accessed_twice_outside_and_inside_subquery(bool async) + => Task.CompletedTask; + + public override Task Select_correlated_filtered_collection_returning_queryable_throws(bool async) + => Task.CompletedTask; + + // test infra issue + public override Task Query_reusing_parameter_with_inner_query_doesnt_declare_duplicate_parameter(bool async) + => Task.CompletedTask; + + public override Task Multiple_includes_with_client_method_around_entity_and_also_projecting_included_collection() + => Task.CompletedTask; + + public override void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result1() + { + } + + public override void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result2() + { + } + + public override void Byte_array_filter_by_length_parameter_compiled() + { + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Year(bool async) + { + await base.Where_DateOnly_Year(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Month(bool async) + { + await base.Where_DateOnly_Month(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_Day(bool async) + { + await base.Where_DateOnly_Day(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfYear(bool async) + { + await base.Where_DateOnly_DayOfYear(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_DayOfWeek(bool async) + { + await base.Where_DateOnly_DayOfWeek(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddYears(bool async) + { + await base.Where_DateOnly_AddYears(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddMonths(bool async) + { + await base.Where_DateOnly_AddMonths(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_DateOnly_AddDays(bool async) + { + await base.Where_DateOnly_AddDays(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Hour(bool async) + { + await base.Where_TimeOnly_Hour(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Minute(bool async) + { + await base.Where_TimeOnly_Minute(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Second(bool async) + { + await base.Where_TimeOnly_Second(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Millisecond(bool async) + { + await base.Where_TimeOnly_Millisecond(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddHours(bool async) + { + await base.Where_TimeOnly_AddHours(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_AddMinutes(bool async) + { + await base.Where_TimeOnly_AddMinutes(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_Add_TimeSpan(bool async) + { + await base.Where_TimeOnly_Add_TimeSpan(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_IsBetween(bool async) + { + await base.Where_TimeOnly_IsBetween(async); + + AssertSql(""); + } + + [ConditionalTheory(Skip = "#24507")] + [MemberData(nameof(IsAsyncData))] + public override async Task Where_TimeOnly_subtract_TimeOnly(bool async) + { + await base.Where_TimeOnly_subtract_TimeOnly(async); + + AssertSql(""); + } + + public override async Task Basic_query_gears(bool async) + { + await base.Basic_query_gears(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g]"); + } + + public override async Task Accessing_derived_property_using_hard_and_soft_cast(bool async) + { + await base.Accessing_derived_property_using_hard_and_soft_cast(async); + + AssertSql( + @"SELECT [l].[Name], [l].[Discriminator], [l].[LocustHordeId], [l].[PeriodEnd], [l].[PeriodStart], [l].[ThreatLevel], [l].[ThreatLevelByte], [l].[ThreatLevelNullableByte], [l].[DefeatedByNickname], [l].[DefeatedBySquadId], [l].[HighCommandId] +FROM [LocustLeaders] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l] +WHERE ([l].[Discriminator] = N'LocustCommander') AND (([l].[HighCommandId] <> 0) OR [l].[HighCommandId] IS NULL)", + // + @"SELECT [l].[Name], [l].[Discriminator], [l].[LocustHordeId], [l].[PeriodEnd], [l].[PeriodStart], [l].[ThreatLevel], [l].[ThreatLevelByte], [l].[ThreatLevelNullableByte], [l].[DefeatedByNickname], [l].[DefeatedBySquadId], [l].[HighCommandId] +FROM [LocustLeaders] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [l] +WHERE ([l].[Discriminator] = N'LocustCommander') AND (([l].[HighCommandId] <> 0) OR [l].[HighCommandId] IS NULL)"); + } + + public override async Task Accessing_property_of_optional_navigation_in_child_projection_works(bool async) + { + await base.Accessing_property_of_optional_navigation_in_child_projection_works(async); + + AssertSql( + @"SELECT CASE + WHEN [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END, [t].[Id], [g].[Nickname], [g].[SquadId], [t0].[Nickname], [t0].[Id], [t0].[SquadId] +FROM [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t] +LEFT JOIN [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] ON ([t].[GearNickName] = [g].[Nickname]) AND ([t].[GearSquadId] = [g].[SquadId]) +LEFT JOIN ( + SELECT [g0].[Nickname], [w].[Id], [g0].[SquadId], [w].[OwnerFullName] + FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] + LEFT JOIN [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g0] ON [w].[OwnerFullName] = [g0].[FullName] +) AS [t0] ON [g].[FullName] = [t0].[OwnerFullName] +ORDER BY [t].[Note], [t].[Id], [g].[Nickname], [g].[SquadId], [t0].[Id], [t0].[Nickname], [t0].[SquadId]"); + } + + public override async Task Acessing_reference_navigation_collection_composition_generates_single_query(bool async) + { + await base.Acessing_reference_navigation_collection_composition_generates_single_query(async); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [t].[Id], [t].[IsAutomatic], [t].[Name], [t].[Id0] +FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] +LEFT JOIN ( + SELECT [w].[Id], [w].[IsAutomatic], [w0].[Name], [w0].[Id] AS [Id0], [w].[OwnerFullName] + FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] + LEFT JOIN [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w0] ON [w].[SynergyWithId] = [w0].[Id] +) AS [t] ON [g].[FullName] = [t].[OwnerFullName] +ORDER BY [g].[Nickname], [g].[SquadId], [t].[Id], [t].[Id0]"); + } + + public override async Task All_with_optional_navigation_is_translated_to_sql(bool async) + { + await base.All_with_optional_navigation_is_translated_to_sql(async); + + AssertSql( + @"SELECT CASE + WHEN NOT EXISTS ( + SELECT 1 + FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] + LEFT JOIN [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t] ON ([g].[Nickname] = [t].[GearNickName]) AND ([g].[SquadId] = [t].[GearSquadId]) + WHERE ([t].[Note] = N'Foo') AND [t].[Note] IS NOT NULL) THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END"); + } + + public override async Task Anonymous_projection_take_followed_by_projecting_single_element_from_collection_navigation(bool async) + { + await base.Anonymous_projection_take_followed_by_projecting_single_element_from_collection_navigation(async); + + AssertSql( + @""); + } + + public override async Task Any_with_optional_navigation_as_subquery_predicate_is_translated_to_sql(bool async) + { + await base.Any_with_optional_navigation_as_subquery_predicate_is_translated_to_sql(async); + + AssertSql( + @"SELECT [s].[Name] +FROM [Squads] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [s] +WHERE NOT (EXISTS ( + SELECT 1 + FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] + LEFT JOIN [Tags] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [t] ON ([g].[Nickname] = [t].[GearNickName]) AND ([g].[SquadId] = [t].[GearSquadId]) + WHERE ([s].[Id] = [g].[SquadId]) AND ([t].[Note] = N'Dom''s Tag')))"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Set_operation_on_temporal_same_ops(bool async) + { + using var ctx = CreateContext(); + var date = new DateTime(2015, 1, 1); + var query = ctx.Set().TemporalAsOf(date).Where(g => g.HasSoulPatch).Concat(ctx.Set().TemporalAsOf(date)); + var expected = async + ? await query.ToListAsync() + : query.ToList(); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2015-01-01T00:00:00.0000000' AS [g] +WHERE [g].[HasSoulPatch] = CAST(1 AS bit) +UNION ALL +SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[PeriodEnd], [g0].[PeriodStart], [g0].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2015-01-01T00:00:00.0000000' AS [g0]"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Set_operation_with_inheritance_on_temporal_same_ops(bool async) + { + using var ctx = CreateContext(); + var date = new DateTime(2015, 1, 1); + var query = ctx.Set().TemporalAsOf(date).Concat(ctx.Set().TemporalAsOf(date)); + var expected = async + ? await query.ToListAsync() + : query.ToList(); + + AssertSql( + @"SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2015-01-01T00:00:00.0000000' AS [g] +WHERE [g].[Discriminator] = N'Officer' +UNION ALL +SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[PeriodEnd], [g0].[PeriodStart], [g0].[Rank] +FROM [Gears] FOR SYSTEM_TIME AS OF '2015-01-01T00:00:00.0000000' AS [g0] +WHERE [g0].[Discriminator] = N'Officer'"); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Set_operation_on_temporal_different_dates(bool async) + { + using var ctx = CreateContext(); + var date1 = new DateTime(2015, 1, 1); + var date2 = new DateTime(2018, 1, 1); + var query = ctx.Set().TemporalAsOf(date1).Where(g => g.HasSoulPatch).Concat(ctx.Set().TemporalAsOf(date2)); + + var message = (await Assert.ThrowsAsync(() => async + ? query.ToListAsync() + : Task.FromResult(query.ToList()))).Message; + + Assert.Equal(SqlServerStrings.TemporalSetOperationOnMismatchedSources(nameof(Gear)), message); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerFixture.cs new file mode 100644 index 00000000000..47314008b05 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerFixture.cs @@ -0,0 +1,258 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.TestModels.ManyToManyModel; +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class TemporalManyToManyQuerySqlServerFixture : ManyToManyQueryFixtureBase + { + protected override string StoreName { get; } = "TemporalManyToManyQueryTest"; + + public DateTime ChangesDate { get; private set; } + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().HasKey( + e => new + { + e.Key1, + e.Key2, + e.Key3 + }); + modelBuilder.Entity().Property(e => e.Id).ValueGeneratedNever(); + modelBuilder.Entity().HasBaseType(); + modelBuilder.Entity().HasBaseType(); + + modelBuilder.Entity() + .HasMany(e => e.Collection) + .WithOne(e => e.CollectionInverse) + .HasForeignKey(e => e.CollectionInverseId); + + modelBuilder.Entity() + .HasOne(e => e.Reference) + .WithOne(e => e.ReferenceInverse) + .HasForeignKey(e => e.ReferenceInverseId); + + // TODO: Remove UsingEntity + modelBuilder.Entity() + .HasMany(e => e.TwoSkipShared) + .WithMany(e => e.OneSkipShared) + .UsingEntity>( + "EntityOneEntityTwo", + r => r.HasOne().WithMany().HasForeignKey("EntityTwoId"), + l => l.HasOne().WithMany().HasForeignKey("EntityOneId")).ToTable(tb => tb.IsTemporal()); + + // Nav:2 Payload:No Join:Concrete Extra:None + modelBuilder.Entity() + .HasMany(e => e.TwoSkip) + .WithMany(e => e.OneSkip) + .UsingEntity( + r => r.HasOne(e => e.Two).WithMany().HasForeignKey(e => e.TwoId), + l => l.HasOne(e => e.One).WithMany().HasForeignKey(e => e.OneId)).ToTable(tb => tb.IsTemporal()); + + // Nav:6 Payload:Yes Join:Concrete Extra:None + modelBuilder.Entity() + .HasMany(e => e.ThreeSkipPayloadFull) + .WithMany(e => e.OneSkipPayloadFull) + .UsingEntity( + r => r.HasOne(x => x.Three).WithMany(e => e.JoinOnePayloadFull), + l => l.HasOne(x => x.One).WithMany(e => e.JoinThreePayloadFull)).ToTable(tb => tb.IsTemporal()); + + // Nav:4 Payload:Yes Join:Shared Extra:None + modelBuilder.Entity() + .HasMany(e => e.ThreeSkipPayloadFullShared) + .WithMany(e => e.OneSkipPayloadFullShared) + .UsingEntity>( + "JoinOneToThreePayloadFullShared", + r => r.HasOne().WithMany(e => e.JoinOnePayloadFullShared).HasForeignKey("ThreeId"), + l => l.HasOne().WithMany(e => e.JoinThreePayloadFullShared).HasForeignKey("OneId")).ToTable(tb => tb.IsTemporal()) + .IndexerProperty("Payload"); + + // Nav:6 Payload:Yes Join:Concrete Extra:Self-Ref + modelBuilder.Entity() + .HasMany(e => e.SelfSkipPayloadLeft) + .WithMany(e => e.SelfSkipPayloadRight) + .UsingEntity( + l => l.HasOne(x => x.Left).WithMany(x => x.JoinSelfPayloadLeft), + r => r.HasOne(x => x.Right).WithMany(x => x.JoinSelfPayloadRight)).ToTable(tb => tb.IsTemporal()); + + // Nav:2 Payload:No Join:Concrete Extra:Inheritance + modelBuilder.Entity() + .HasMany(e => e.BranchSkip) + .WithMany(e => e.OneSkip) + .UsingEntity( + r => r.HasOne().WithMany(), + l => l.HasOne().WithMany()).ToTable(tb => tb.IsTemporal()); + + modelBuilder.Entity() + .HasOne(e => e.Reference) + .WithOne(e => e.ReferenceInverse) + .HasForeignKey(e => e.ReferenceInverseId); + + modelBuilder.Entity() + .HasMany(e => e.Collection) + .WithOne(e => e.CollectionInverse) + .HasForeignKey(e => e.CollectionInverseId); + + // Nav:6 Payload:No Join:Concrete Extra:None + modelBuilder.Entity() + .HasMany(e => e.ThreeSkipFull) + .WithMany(e => e.TwoSkipFull) + .UsingEntity( + r => r.HasOne(x => x.Three).WithMany(e => e.JoinTwoFull), + l => l.HasOne(x => x.Two).WithMany(e => e.JoinThreeFull)).ToTable(tb => tb.IsTemporal()); + + // Nav:2 Payload:No Join:Shared Extra:Self-ref + // TODO: Remove UsingEntity + modelBuilder.Entity() + .HasMany(e => e.SelfSkipSharedLeft) + .WithMany(e => e.SelfSkipSharedRight) + .UsingEntity>( + "JoinTwoSelfShared", + l => l.HasOne().WithMany().HasForeignKey("LeftId"), + r => r.HasOne().WithMany().HasForeignKey("RightId")).ToTable(tb => tb.IsTemporal()); + + // Nav:2 Payload:No Join:Shared Extra:CompositeKey + // TODO: Remove UsingEntity + modelBuilder.Entity() + .HasMany(e => e.CompositeKeySkipShared) + .WithMany(e => e.TwoSkipShared) + .UsingEntity>( + "JoinTwoToCompositeKeyShared", + r => r.HasOne().WithMany().HasForeignKey("CompositeId1", "CompositeId2", "CompositeId3"), + l => l.HasOne().WithMany().HasForeignKey("TwoId")).ToTable(tb => tb.IsTemporal()) + .HasKey("TwoId", "CompositeId1", "CompositeId2", "CompositeId3"); + + // Nav:6 Payload:No Join:Concrete Extra:CompositeKey + modelBuilder.Entity() + .HasMany(e => e.CompositeKeySkipFull) + .WithMany(e => e.ThreeSkipFull) + .UsingEntity( + l => l.HasOne(x => x.Composite).WithMany(x => x.JoinThreeFull).HasForeignKey( + e => new + { + e.CompositeId1, + e.CompositeId2, + e.CompositeId3 + }).IsRequired(), + r => r.HasOne(x => x.Three).WithMany(x => x.JoinCompositeKeyFull).IsRequired()).ToTable(tb => tb.IsTemporal()); + + // Nav:2 Payload:No Join:Shared Extra:Inheritance + // TODO: Remove UsingEntity + modelBuilder.Entity().HasMany(e => e.RootSkipShared).WithMany(e => e.ThreeSkipShared) + .UsingEntity>( + "EntityRootEntityThree", + r => r.HasOne().WithMany().HasForeignKey("EntityRootId"), + l => l.HasOne().WithMany().HasForeignKey("EntityThreeId")).ToTable(tb => tb.IsTemporal()); + + // Nav:2 Payload:No Join:Shared Extra:Inheritance,CompositeKey + // TODO: Remove UsingEntity + modelBuilder.Entity() + .HasMany(e => e.RootSkipShared) + .WithMany(e => e.CompositeKeySkipShared) + .UsingEntity>( + "JoinCompositeKeyToRootShared", + r => r.HasOne().WithMany().HasForeignKey("RootId"), + l => l.HasOne().WithMany().HasForeignKey("CompositeId1", "CompositeId2", "CompositeId3")).ToTable(tb => tb.IsTemporal()) + .HasKey("CompositeId1", "CompositeId2", "CompositeId3", "RootId"); + + // Nav:6 Payload:No Join:Concrete Extra:Inheritance,CompositeKey + modelBuilder.Entity() + .HasMany(e => e.LeafSkipFull) + .WithMany(e => e.CompositeKeySkipFull) + .UsingEntity( + r => r.HasOne(x => x.Leaf).WithMany(x => x.JoinCompositeKeyFull), + l => l.HasOne(x => x.Composite).WithMany(x => x.JoinLeafFull).HasForeignKey( + e => new + { + e.CompositeId1, + e.CompositeId2, + e.CompositeId3 + })).ToTable(tb => tb.IsTemporal()) + .HasKey( + e => new + { + e.CompositeId1, + e.CompositeId2, + e.CompositeId3, + e.LeafId + }); + + modelBuilder.SharedTypeEntity( + "PST", b => + { + b.IndexerProperty("Id").ValueGeneratedNever(); + b.IndexerProperty("Payload"); + }); + } + + protected override void Seed(ManyToManyContext context) + { + base.Seed(context); + + ChangesDate = new DateTime(2010, 1, 1); + + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is EntityThree).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is EntityTwo).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is EntityOne).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is EntityCompositeKey).Select(e => e.Entity)); + context.RemoveRange(context.ChangeTracker.Entries().Where(e => e.Entity is EntityRoot).Select(e => e.Entity)); + context.SaveChanges(); + + var historyTableInfos = new List<(string table, string historyTable)>() + { + ("EntityCompositeKeys", "EntityCompositeKeyHistory"), + ("EntityOneEntityTwo", "EntityOneEntityTwoHistory"), + ("EntityOnes", "EntityOneHistory"), + ("EntityTwos", "EntityTwoHistory"), + ("EntityThrees", "EntityThreeHistory"), + ("EntityRoots", "EntityRootHistory"), + ("EntityRootEntityThree", "EntityRootEntityThreeHistory"), + + ("JoinCompositeKeyToLeaf", "JoinCompositeKeyToLeafHistory"), + ("JoinCompositeKeyToRootShared", "JoinCompositeKeyToRootSharedHistory"), + ("JoinOneSelfPayload", "JoinOneSelfPayloadHistory"), + ("JoinOneToBranch", "JoinOneToBranchHistory"), + ("JoinOneToThreePayloadFull", "JoinOneToThreePayloadFullHistory"), + ("JoinOneToThreePayloadFullShared", "JoinOneToThreePayloadFullSharedHistory"), + ("JoinOneToTwo", "JoinOneToTwoHistory"), + ("JoinThreeToCompositeKeyFull", "JoinThreeToCompositeKeyFullHistory"), + ("JoinTwoSelfShared", "JoinTwoSelfSharedHistory"), + ("JoinTwoToCompositeKeyShared", "JoinTwoToCompositeKeySharedHistory"), + ("JoinTwoToThree", "JoinTwoToThreeHistory"), + }; + + foreach (var historyTableInfo in historyTableInfos) + { + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = OFF)"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] DROP PERIOD FOR SYSTEM_TIME"); + + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodStart = '2000-01-01T01:00:00.0000000Z'"); + context.Database.ExecuteSqlRaw($"UPDATE [{historyTableInfo.historyTable}] SET PeriodEnd = '2020-07-01T07:00:00.0000000Z'"); + + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd])"); + context.Database.ExecuteSqlRaw($"ALTER TABLE [{historyTableInfo.table}] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [dbo].[{historyTableInfo.historyTable}]))"); + } + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerTest.cs new file mode 100644 index 00000000000..39c7301203d --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalManyToManyQuerySqlServerTest.cs @@ -0,0 +1,1029 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + [SqlServerCondition(SqlServerCondition.SupportsTemporalTablesCascadeDelete)] + public class TemporalManyToManyQuerySqlServerTest : ManyToManyQueryRelationalTestBase + { +#pragma warning disable IDE0060 // Remove unused parameter + public TemporalManyToManyQuerySqlServerTest(TemporalManyToManyQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) +#pragma warning restore IDE0060 // Remove unused parameter + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override Expression RewriteServerQueryExpression(Expression serverQueryExpression) + { + var temporalEntityTypes = new List + { + typeof(TestModels.ManyToManyModel.EntityOne), + typeof(TestModels.ManyToManyModel.EntityTwo), + typeof(TestModels.ManyToManyModel.EntityThree), + typeof(TestModels.ManyToManyModel.EntityCompositeKey), + typeof(TestModels.ManyToManyModel.EntityRoot), + typeof(TestModels.ManyToManyModel.EntityBranch), + typeof(TestModels.ManyToManyModel.EntityLeaf), + }; + + var rewriter = new TemporalPointInTimeQueryRewriter(Fixture.ChangesDate, temporalEntityTypes); + + return rewriter.Visit(serverQueryExpression); + } + + public override async Task Skip_navigation_all(bool async) + { + await base.Skip_navigation_all(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +WHERE NOT EXISTS ( + SELECT 1 + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[TwoId] = [e0].[Id] + WHERE ([e].[Id] = [j].[OneId]) AND NOT ([e0].[Name] LIKE N'%B%'))"); + } + + public override async Task Skip_navigation_any_without_predicate(bool async) + { + await base.Skip_navigation_any_without_predicate(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +WHERE EXISTS ( + SELECT 1 + FROM [JoinOneToThreePayloadFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[ThreeId] = [e0].[Id] + WHERE ([e].[Id] = [j].[OneId]) AND ([e0].[Name] LIKE N'%B%'))"); + } + + public override async Task Skip_navigation_any_with_predicate(bool async) + { + await base.Skip_navigation_any_with_predicate(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +WHERE EXISTS ( + SELECT 1 + FROM [EntityOneEntityTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[EntityTwoId] = [e1].[Id] + WHERE ([e].[Id] = [e0].[EntityOneId]) AND ([e1].[Name] LIKE N'%B%'))"); + } + + public override async Task Skip_navigation_contains(bool async) + { + await base.Skip_navigation_contains(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +WHERE EXISTS ( + SELECT 1 + FROM [JoinOneToThreePayloadFullShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[ThreeId] = [e0].[Id] + WHERE ([e].[Id] = [j].[OneId]) AND ([e0].[Id] = 1))"); + } + + public override async Task Skip_navigation_count_without_predicate(bool async) + { + await base.Skip_navigation_count_without_predicate(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +WHERE ( + SELECT COUNT(*) + FROM [JoinOneSelfPayload] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[LeftId] = [e0].[Id] + WHERE [e].[Id] = [j].[RightId]) > 0"); + } + + public override async Task Skip_navigation_count_with_predicate(bool async) + { + await base.Skip_navigation_count_with_predicate(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +ORDER BY ( + SELECT COUNT(*) + FROM [JoinOneToBranch] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN ( + SELECT [e0].[Id], [e0].[Discriminator], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[Number], [e0].[IsGreen] + FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + WHERE [e0].[Discriminator] IN (N'EntityBranch', N'EntityLeaf') + ) AS [t] ON [j].[EntityBranchId] = [t].[Id] + WHERE ([e].[Id] = [j].[EntityOneId]) AND ([t].[Name] IS NOT NULL AND ([t].[Name] LIKE N'L%'))), [e].[Id]"); + } + + public override async Task Skip_navigation_long_count_without_predicate(bool async) + { + await base.Skip_navigation_long_count_without_predicate(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +WHERE ( + SELECT COUNT_BIG(*) + FROM [JoinTwoToThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[ThreeId] = [e0].[Id] + WHERE [e].[Id] = [j].[TwoId]) > CAST(0 AS bigint)"); + } + + public override async Task Skip_navigation_long_count_with_predicate(bool async) + { + await base.Skip_navigation_long_count_with_predicate(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +ORDER BY ( + SELECT COUNT_BIG(*) + FROM [JoinTwoSelfShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[LeftId] = [e0].[Id] + WHERE ([e].[Id] = [j].[RightId]) AND ([e0].[Name] IS NOT NULL AND ([e0].[Name] LIKE N'L%'))) DESC, [e].[Id]"); + } + + public override async Task Skip_navigation_select_many_average(bool async) + { + await base.Skip_navigation_select_many_average(async); + + AssertSql( + @"SELECT AVG(CAST([t].[Key1] AS float)) +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [e0].[Key1], [j].[TwoId] + FROM [JoinTwoToCompositeKeyShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON (([j].[CompositeId1] = [e0].[Key1]) AND ([j].[CompositeId2] = [e0].[Key2])) AND ([j].[CompositeId3] = [e0].[Key3]) +) AS [t] ON [e].[Id] = [t].[TwoId]"); + } + + public override async Task Skip_navigation_select_many_max(bool async) + { + await base.Skip_navigation_select_many_max(async); + + AssertSql( + @"SELECT MAX([t].[Key1]) +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [e0].[Key1], [j].[ThreeId] + FROM [JoinThreeToCompositeKeyFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON (([j].[CompositeId1] = [e0].[Key1]) AND ([j].[CompositeId2] = [e0].[Key2])) AND ([j].[CompositeId3] = [e0].[Key3]) +) AS [t] ON [e].[Id] = [t].[ThreeId]"); + } + + public override async Task Skip_navigation_select_many_min(bool async) + { + await base.Skip_navigation_select_many_min(async); + + AssertSql( + @"SELECT MIN([t].[Id]) +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e0].[EntityThreeId] + FROM [EntityRootEntityThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + INNER JOIN [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[EntityRootId] = [e1].[Id] +) AS [t] ON [e].[Id] = [t].[EntityThreeId]"); + } + + public override async Task Skip_navigation_select_many_sum(bool async) + { + await base.Skip_navigation_select_many_sum(async); + + AssertSql( + @"SELECT COALESCE(SUM([t].[Key1]), 0) +FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [e0].[Key1], [j].[RootId] + FROM [JoinCompositeKeyToRootShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON (([j].[CompositeId1] = [e0].[Key1]) AND ([j].[CompositeId2] = [e0].[Key2])) AND ([j].[CompositeId3] = [e0].[Key3]) +) AS [t] ON [e].[Id] = [t].[RootId]"); + } + + public override async Task Skip_navigation_select_subquery_average(bool async) + { + await base.Skip_navigation_select_subquery_average(async); + + AssertSql( + @"SELECT ( + SELECT AVG(CAST([e0].[Key1] AS float)) + FROM [JoinCompositeKeyToLeaf] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON (([j].[CompositeId1] = [e0].[Key1]) AND ([j].[CompositeId2] = [e0].[Key2])) AND ([j].[CompositeId3] = [e0].[Key3]) + WHERE [e].[Id] = [j].[LeafId]) +FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +WHERE [e].[Discriminator] = N'EntityLeaf'"); + } + + public override async Task Skip_navigation_select_subquery_max(bool async) + { + await base.Skip_navigation_select_subquery_max(async); + + AssertSql( + @"SELECT ( + SELECT MAX([e0].[Id]) + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + WHERE [e].[Id] = [j].[TwoId]) +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e]"); + } + + public override async Task Skip_navigation_select_subquery_min(bool async) + { + await base.Skip_navigation_select_subquery_min(async); + + AssertSql( + @"SELECT ( + SELECT MIN([e0].[Id]) + FROM [JoinOneToThreePayloadFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + WHERE [e].[Id] = [j].[ThreeId]) +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e]"); + } + + public override async Task Skip_navigation_select_subquery_sum(bool async) + { + await base.Skip_navigation_select_subquery_sum(async); + + AssertSql( + @"SELECT ( + SELECT COALESCE(SUM([e1].[Id]), 0) + FROM [EntityOneEntityTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[EntityOneId] = [e1].[Id] + WHERE [e].[Id] = [e0].[EntityTwoId]) +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e]"); + } + + public override async Task Skip_navigation_order_by_first_or_default(bool async) + { + await base.Skip_navigation_order_by_first_or_default(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[Id], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ThreeId] + FROM ( + SELECT [e0].[Id], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [j].[ThreeId], ROW_NUMBER() OVER(PARTITION BY [j].[ThreeId] ORDER BY [e0].[Id]) AS [row] + FROM [JoinOneToThreePayloadFullShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + ) AS [t] + WHERE [t].[row] <= 1 +) AS [t0] ON [e].[Id] = [t0].[ThreeId]"); + } + + public override async Task Skip_navigation_order_by_single_or_default(bool async) + { + await base.Skip_navigation_order_by_single_or_default(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +OUTER APPLY ( + SELECT TOP(1) [t].[Id], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart] + FROM ( + SELECT TOP(1) [e0].[Id], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart] + FROM [JoinOneSelfPayload] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[RightId] = [e0].[Id] + WHERE [e].[Id] = [j].[LeftId] + ORDER BY [e0].[Id] + ) AS [t] + ORDER BY [t].[Id] +) AS [t0]"); + } + + public override async Task Skip_navigation_order_by_last_or_default(bool async) + { + await base.Skip_navigation_order_by_last_or_default(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart] +FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[Id], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[EntityBranchId] + FROM ( + SELECT [e0].[Id], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [j].[EntityBranchId], ROW_NUMBER() OVER(PARTITION BY [j].[EntityBranchId] ORDER BY [e0].[Id] DESC) AS [row] + FROM [JoinOneToBranch] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[EntityOneId] = [e0].[Id] + ) AS [t] + WHERE [t].[row] <= 1 +) AS [t0] ON [e].[Id] = [t0].[EntityBranchId] +WHERE [e].[Discriminator] IN (N'EntityBranch', N'EntityLeaf')"); + } + + public override async Task Skip_navigation_order_by_reverse_first_or_default(bool async) + { + await base.Skip_navigation_order_by_reverse_first_or_default(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[ReferenceInverseId] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ReferenceInverseId], [t].[ThreeId] + FROM ( + SELECT [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId], [j].[ThreeId], ROW_NUMBER() OVER(PARTITION BY [j].[ThreeId] ORDER BY [e0].[Id] DESC) AS [row] + FROM [JoinTwoToThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[TwoId] = [e0].[Id] + ) AS [t] + WHERE [t].[row] <= 1 +) AS [t0] ON [e].[Id] = [t0].[ThreeId]"); + } + + public override async Task Skip_navigation_cast(bool async) + { + await base.Skip_navigation_cast(async); + + AssertSql( + @"SELECT [e].[Key1], [e].[Key2], [e].[Key3], [t0].[Id], [t0].[Discriminator], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Number], [t0].[IsGreen], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[LeafId] +FROM [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[Id], [t].[Discriminator], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[Number], [t].[IsGreen], [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[LeafId] + FROM [JoinCompositeKeyToLeaf] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN ( + SELECT [e0].[Id], [e0].[Discriminator], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[Number], [e0].[IsGreen] + FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + WHERE [e0].[Discriminator] = N'EntityLeaf' + ) AS [t] ON [j].[LeafId] = [t].[Id] +) AS [t0] ON (([e].[Key1] = [t0].[CompositeId1]) AND ([e].[Key2] = [t0].[CompositeId2])) AND ([e].[Key3] = [t0].[CompositeId3]) +ORDER BY [e].[Key1], [e].[Key2], [e].[Key3], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[LeafId], [t0].[Id]"); + } + + public override async Task Skip_navigation_of_type(bool async) + { + await base.Skip_navigation_of_type(async); + + AssertSql( + @"SELECT [e].[Key1], [e].[Key2], [e].[Key3], [t].[Id], [t].[Discriminator], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[Number], [t].[IsGreen], [t].[CompositeId1], [t].[CompositeId2], [t].[CompositeId3], [t].[RootId] +FROM [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [e0].[Id], [e0].[Discriminator], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[Number], [e0].[IsGreen], [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[RootId] + FROM [JoinCompositeKeyToRootShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[RootId] = [e0].[Id] + WHERE [e0].[Discriminator] = N'EntityLeaf' +) AS [t] ON (([e].[Key1] = [t].[CompositeId1]) AND ([e].[Key2] = [t].[CompositeId2])) AND ([e].[Key3] = [t].[CompositeId3]) +ORDER BY [e].[Key1], [e].[Key2], [e].[Key3], [t].[CompositeId1], [t].[CompositeId2], [t].[CompositeId3], [t].[RootId], [t].[Id]"); + } + + public override async Task Join_with_skip_navigation(bool async) + { + await base.Join_with_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [e].[Id] = ( + SELECT TOP(1) [e1].[Id] + FROM [JoinTwoSelfShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j].[RightId] = [e1].[Id] + WHERE [e0].[Id] = [j].[LeftId] + ORDER BY [e1].[Id])"); + } + + public override async Task Left_join_with_skip_navigation(bool async) + { + await base.Left_join_with_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Key1], [e].[Key2], [e].[Key3], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e0].[Key1], [e0].[Key2], [e0].[Key3], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart] +FROM [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON ( + SELECT TOP(1) [e1].[Id] + FROM [JoinTwoToCompositeKeyShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j].[TwoId] = [e1].[Id] + WHERE (([e].[Key1] = [j].[CompositeId1]) AND ([e].[Key2] = [j].[CompositeId2])) AND ([e].[Key3] = [j].[CompositeId3]) + ORDER BY [e1].[Id]) = ( + SELECT TOP(1) [e2].[Id] + FROM [JoinThreeToCompositeKeyFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e2] ON [j0].[ThreeId] = [e2].[Id] + WHERE (([e0].[Key1] = [j0].[CompositeId1]) AND ([e0].[Key2] = [j0].[CompositeId2])) AND ([e0].[Key3] = [j0].[CompositeId3]) + ORDER BY [e2].[Id]) +ORDER BY [e].[Key1], [e0].[Key1], [e].[Key2], [e0].[Key2]"); + } + + public override async Task Select_many_over_skip_navigation(bool async) + { + await base.Select_many_over_skip_navigation(async); + + AssertSql( + @"SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ReferenceInverseId] +FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[PeriodEnd], [e1].[PeriodStart], [e1].[ReferenceInverseId], [e0].[EntityRootId] + FROM [EntityRootEntityThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[EntityThreeId] = [e1].[Id] +) AS [t] ON [e].[Id] = [t].[EntityRootId]"); + } + + public override async Task Select_many_over_skip_navigation_where(bool async) + { + await base.Select_many_over_skip_navigation_where(async); + + AssertSql( + @"SELECT [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ReferenceInverseId] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId], [j].[OneId] + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[TwoId] = [e0].[Id] +) AS [t] ON [e].[Id] = [t].[OneId]"); + } + + public override async Task Select_many_over_skip_navigation_order_by_skip(bool async) + { + await base.Select_many_over_skip_navigation_order_by_skip(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[CollectionInverseId], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[ReferenceInverseId] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ReferenceInverseId], [t].[OneId] + FROM ( + SELECT [e0].[Id], [e0].[CollectionInverseId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId], [j].[OneId], ROW_NUMBER() OVER(PARTITION BY [j].[OneId] ORDER BY [e0].[Id]) AS [row] + FROM [JoinOneToThreePayloadFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[ThreeId] = [e0].[Id] + ) AS [t] + WHERE 2 < [t].[row] +) AS [t0] ON [e].[Id] = [t0].[OneId]"); + } + + public override async Task Select_many_over_skip_navigation_order_by_take(bool async) + { + await base.Select_many_over_skip_navigation_order_by_take(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[ReferenceInverseId] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ReferenceInverseId], [t].[EntityOneId] + FROM ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name], [e1].[PeriodEnd], [e1].[PeriodStart], [e1].[ReferenceInverseId], [e0].[EntityOneId], ROW_NUMBER() OVER(PARTITION BY [e0].[EntityOneId] ORDER BY [e1].[Id]) AS [row] + FROM [EntityOneEntityTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[EntityTwoId] = [e1].[Id] + ) AS [t] + WHERE [t].[row] <= 2 +) AS [t0] ON [e].[Id] = [t0].[EntityOneId]"); + } + + public override async Task Select_many_over_skip_navigation_order_by_skip_take(bool async) + { + await base.Select_many_over_skip_navigation_order_by_skip_take(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[CollectionInverseId], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[ReferenceInverseId] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ReferenceInverseId], [t].[OneId] + FROM ( + SELECT [e0].[Id], [e0].[CollectionInverseId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId], [j].[OneId], ROW_NUMBER() OVER(PARTITION BY [j].[OneId] ORDER BY [e0].[Id]) AS [row] + FROM [JoinOneToThreePayloadFullShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[ThreeId] = [e0].[Id] + ) AS [t] + WHERE (2 < [t].[row]) AND ([t].[row] <= 5) +) AS [t0] ON [e].[Id] = [t0].[OneId]"); + } + + public override async Task Select_many_over_skip_navigation_of_type(bool async) + { + await base.Select_many_over_skip_navigation_of_type(async); + + AssertSql( + @"SELECT [t].[Id], [t].[Discriminator], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[Number], [t].[IsGreen] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [e1].[Id], [e1].[Discriminator], [e1].[Name], [e1].[PeriodEnd], [e1].[PeriodStart], [e1].[Number], [e1].[IsGreen], [e0].[EntityThreeId] + FROM [EntityRootEntityThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + INNER JOIN [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[EntityRootId] = [e1].[Id] + WHERE [e1].[Discriminator] IN (N'EntityBranch', N'EntityLeaf') +) AS [t] ON [e].[Id] = [t].[EntityThreeId]"); + } + + public override async Task Select_many_over_skip_navigation_cast(bool async) + { + await base.Select_many_over_skip_navigation_cast(async); + + AssertSql( + @"SELECT [t0].[Id], [t0].[Discriminator], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Number], [t0].[IsGreen] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +INNER JOIN ( + SELECT [t].[Id], [t].[Discriminator], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[Number], [t].[IsGreen], [j].[EntityOneId] + FROM [JoinOneToBranch] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN ( + SELECT [e0].[Id], [e0].[Discriminator], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[Number], [e0].[IsGreen] + FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + WHERE [e0].[Discriminator] IN (N'EntityBranch', N'EntityLeaf') + ) AS [t] ON [j].[EntityBranchId] = [t].[Id] +) AS [t0] ON [e].[Id] = [t0].[EntityOneId]"); + } + + public override async Task Select_skip_navigation(bool async) + { + await base.Select_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Id], [t].[Id], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[LeftId], [t].[RightId] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [e0].[Id], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [j].[LeftId], [j].[RightId] + FROM [JoinOneSelfPayload] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[LeftId] = [e0].[Id] +) AS [t] ON [e].[Id] = [t].[RightId] +ORDER BY [e].[Id], [t].[LeftId], [t].[RightId], [t].[Id]"); + } + + public override async Task Select_skip_navigation_multiple(bool async) + { + await base.Select_skip_navigation_multiple(async); + + AssertSql( + @"SELECT [e].[Id], [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ReferenceInverseId], [t].[ThreeId], [t].[TwoId], [t0].[Id], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[ReferenceInverseId], [t0].[LeftId], [t0].[RightId], [t1].[Key1], [t1].[Key2], [t1].[Key3], [t1].[Name], [t1].[PeriodEnd], [t1].[PeriodStart], [t1].[TwoId], [t1].[CompositeId1], [t1].[CompositeId2], [t1].[CompositeId3] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [e0].[Id], [e0].[CollectionInverseId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId], [j].[ThreeId], [j].[TwoId] + FROM [JoinTwoToThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[ThreeId] = [e0].[Id] +) AS [t] ON [e].[Id] = [t].[TwoId] +LEFT JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name], [e1].[PeriodEnd], [e1].[PeriodStart], [e1].[ReferenceInverseId], [j0].[LeftId], [j0].[RightId] + FROM [JoinTwoSelfShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[LeftId] = [e1].[Id] +) AS [t0] ON [e].[Id] = [t0].[RightId] +LEFT JOIN ( + SELECT [e2].[Key1], [e2].[Key2], [e2].[Key3], [e2].[Name], [e2].[PeriodEnd], [e2].[PeriodStart], [j1].[TwoId], [j1].[CompositeId1], [j1].[CompositeId2], [j1].[CompositeId3] + FROM [JoinTwoToCompositeKeyShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j1] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e2] ON (([j1].[CompositeId1] = [e2].[Key1]) AND ([j1].[CompositeId2] = [e2].[Key2])) AND ([j1].[CompositeId3] = [e2].[Key3]) +) AS [t1] ON [e].[Id] = [t1].[TwoId] +ORDER BY [e].[Id], [t].[ThreeId], [t].[TwoId], [t].[Id], [t0].[LeftId], [t0].[RightId], [t0].[Id], [t1].[TwoId], [t1].[CompositeId1], [t1].[CompositeId2], [t1].[CompositeId3], [t1].[Key1], [t1].[Key2], [t1].[Key3]"); + } + + public override async Task Select_skip_navigation_first_or_default(bool async) + { + await base.Select_skip_navigation_first_or_default(async); + + AssertSql( + @"SELECT [t0].[Key1], [t0].[Key2], [t0].[Key3], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[Key1], [t].[Key2], [t].[Key3], [t].[Name], [t].[PeriodEnd], [t].[PeriodStart], [t].[ThreeId] + FROM ( + SELECT [e0].[Key1], [e0].[Key2], [e0].[Key3], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [j].[ThreeId], ROW_NUMBER() OVER(PARTITION BY [j].[ThreeId] ORDER BY [e0].[Key1], [e0].[Key2]) AS [row] + FROM [JoinThreeToCompositeKeyFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON (([j].[CompositeId1] = [e0].[Key1]) AND ([j].[CompositeId2] = [e0].[Key2])) AND ([j].[CompositeId3] = [e0].[Key3]) + ) AS [t] + WHERE [t].[row] <= 1 +) AS [t0] ON [e].[Id] = [t0].[ThreeId] +ORDER BY [e].[Id]"); + } + + public override async Task Include_skip_navigation(bool async) + { + await base.Include_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Key1], [e].[Key2], [e].[Key3], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [t].[CompositeId1], [t].[CompositeId2], [t].[CompositeId3], [t].[RootId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[Discriminator], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[Number], [t].[IsGreen] +FROM [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[RootId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Discriminator], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e0].[Number], [e0].[IsGreen] + FROM [JoinCompositeKeyToRootShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[RootId] = [e0].[Id] +) AS [t] ON (([e].[Key1] = [t].[CompositeId1]) AND ([e].[Key2] = [t].[CompositeId2])) AND ([e].[Key3] = [t].[CompositeId3]) +ORDER BY [e].[Key1], [e].[Key2], [e].[Key3], [t].[CompositeId1], [t].[CompositeId2], [t].[CompositeId3], [t].[RootId], [t].[Id]"); + } + + public override async Task Include_skip_navigation_then_reference(bool async) + { + await base.Include_skip_navigation_then_reference(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t].[OneId], [t].[TwoId], [t].[JoinOneToTwoExtraId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[Id0], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name0], [t].[PeriodEnd1], [t].[PeriodStart1], [t].[ReferenceInverseId] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[TwoId], [j].[JoinOneToTwoExtraId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e1].[Id] AS [Id0], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name] AS [Name0], [e1].[PeriodEnd] AS [PeriodEnd1], [e1].[PeriodStart] AS [PeriodStart1], [e1].[ReferenceInverseId] + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + LEFT JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[Id] = [e1].[ReferenceInverseId] +) AS [t] ON [e].[Id] = [t].[TwoId] +ORDER BY [e].[Id], [t].[OneId], [t].[TwoId], [t].[Id], [t].[Id0]"); + } + + public override async Task Include_skip_navigation_then_include_skip_navigation(bool async) + { + await base.Include_skip_navigation_then_include_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Key1], [e].[Key2], [e].[Key3], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [t1].[CompositeId1], [t1].[CompositeId2], [t1].[CompositeId3], [t1].[LeafId], [t1].[PeriodEnd], [t1].[PeriodStart], [t1].[Id], [t1].[Discriminator], [t1].[Name], [t1].[PeriodEnd0], [t1].[PeriodStart0], [t1].[Number], [t1].[IsGreen], [t1].[EntityBranchId], [t1].[EntityOneId], [t1].[PeriodEnd1], [t1].[PeriodStart1], [t1].[Id0], [t1].[Name0], [t1].[PeriodEnd00], [t1].[PeriodStart00] +FROM [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[LeafId], [j].[PeriodEnd], [j].[PeriodStart], [t].[Id], [t].[Discriminator], [t].[Name], [t].[PeriodEnd] AS [PeriodEnd0], [t].[PeriodStart] AS [PeriodStart0], [t].[Number], [t].[IsGreen], [t0].[EntityBranchId], [t0].[EntityOneId], [t0].[PeriodEnd] AS [PeriodEnd1], [t0].[PeriodStart] AS [PeriodStart1], [t0].[Id] AS [Id0], [t0].[Name] AS [Name0], [t0].[PeriodEnd0] AS [PeriodEnd00], [t0].[PeriodStart0] AS [PeriodStart00] + FROM [JoinCompositeKeyToLeaf] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN ( + SELECT [e0].[Id], [e0].[Discriminator], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[Number], [e0].[IsGreen] + FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + WHERE [e0].[Discriminator] = N'EntityLeaf' + ) AS [t] ON [j].[LeafId] = [t].[Id] + LEFT JOIN ( + SELECT [j0].[EntityBranchId], [j0].[EntityOneId], [j0].[PeriodEnd], [j0].[PeriodStart], [e1].[Id], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0] + FROM [JoinOneToBranch] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[EntityOneId] = [e1].[Id] + ) AS [t0] ON [t].[Id] = [t0].[EntityBranchId] +) AS [t1] ON (([e].[Key1] = [t1].[CompositeId1]) AND ([e].[Key2] = [t1].[CompositeId2])) AND ([e].[Key3] = [t1].[CompositeId3]) +ORDER BY [e].[Key1], [e].[Key2], [e].[Key3], [t1].[CompositeId1], [t1].[CompositeId2], [t1].[CompositeId3], [t1].[LeafId], [t1].[Id], [t1].[EntityBranchId], [t1].[EntityOneId], [t1].[Id0]"); + } + + public override async Task Include_skip_navigation_then_include_reference_and_skip_navigation(bool async) + { + await base.Include_skip_navigation_then_include_reference_and_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t0].[OneId], [t0].[ThreeId], [t0].[Payload], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Id], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[Id0], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name0], [t0].[PeriodEnd1], [t0].[PeriodStart1], [t0].[ReferenceInverseId], [t0].[LeftId], [t0].[RightId], [t0].[Payload0], [t0].[PeriodEnd2], [t0].[PeriodStart2], [t0].[Id1], [t0].[Name1], [t0].[PeriodEnd00], [t0].[PeriodStart00] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[ThreeId], [j].[Payload], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e1].[Id] AS [Id0], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name] AS [Name0], [e1].[PeriodEnd] AS [PeriodEnd1], [e1].[PeriodStart] AS [PeriodStart1], [e1].[ReferenceInverseId], [t].[LeftId], [t].[RightId], [t].[Payload] AS [Payload0], [t].[PeriodEnd] AS [PeriodEnd2], [t].[PeriodStart] AS [PeriodStart2], [t].[Id] AS [Id1], [t].[Name] AS [Name1], [t].[PeriodEnd0] AS [PeriodEnd00], [t].[PeriodStart0] AS [PeriodStart00] + FROM [JoinOneToThreePayloadFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + LEFT JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[Id] = [e1].[ReferenceInverseId] + LEFT JOIN ( + SELECT [j0].[LeftId], [j0].[RightId], [j0].[Payload], [j0].[PeriodEnd], [j0].[PeriodStart], [e2].[Id], [e2].[Name], [e2].[PeriodEnd] AS [PeriodEnd0], [e2].[PeriodStart] AS [PeriodStart0] + FROM [JoinOneSelfPayload] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e2] ON [j0].[RightId] = [e2].[Id] + ) AS [t] ON [e0].[Id] = [t].[LeftId] +) AS [t0] ON [e].[Id] = [t0].[ThreeId] +ORDER BY [e].[Id], [t0].[OneId], [t0].[ThreeId], [t0].[Id], [t0].[Id0], [t0].[LeftId], [t0].[RightId], [t0].[Id1]"); + } + + public override async Task Include_skip_navigation_and_reference(bool async) + { + await base.Include_skip_navigation_and_reference(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [e0].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [e0].[CollectionInverseId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [e].[Id] = [e0].[ReferenceInverseId] +LEFT JOIN ( + SELECT [e1].[EntityOneId], [e1].[EntityTwoId], [e1].[PeriodEnd], [e1].[PeriodStart], [e2].[Id], [e2].[Name], [e2].[PeriodEnd] AS [PeriodEnd0], [e2].[PeriodStart] AS [PeriodStart0] + FROM [EntityOneEntityTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e2] ON [e1].[EntityOneId] = [e2].[Id] +) AS [t] ON [e].[Id] = [t].[EntityTwoId] +ORDER BY [e].[Id], [e0].[Id], [t].[EntityOneId], [t].[EntityTwoId], [t].[Id]"); + } + + public override async Task Include_skip_navigation_then_include_inverse_works_for_tracking_query(bool async) + { + await base.Include_skip_navigation_then_include_inverse_works_for_tracking_query(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t0].[OneId], [t0].[ThreeId], [t0].[Payload], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Id], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[OneId0], [t0].[ThreeId0], [t0].[Payload0], [t0].[PeriodEnd1], [t0].[PeriodStart1], [t0].[Id0], [t0].[CollectionInverseId], [t0].[Name0], [t0].[PeriodEnd00], [t0].[PeriodStart00], [t0].[ReferenceInverseId] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[ThreeId], [j].[Payload], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [t].[OneId] AS [OneId0], [t].[ThreeId] AS [ThreeId0], [t].[Payload] AS [Payload0], [t].[PeriodEnd] AS [PeriodEnd1], [t].[PeriodStart] AS [PeriodStart1], [t].[Id] AS [Id0], [t].[CollectionInverseId], [t].[Name] AS [Name0], [t].[PeriodEnd0] AS [PeriodEnd00], [t].[PeriodStart0] AS [PeriodStart00], [t].[ReferenceInverseId] + FROM [JoinOneToThreePayloadFullShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + LEFT JOIN ( + SELECT [j0].[OneId], [j0].[ThreeId], [j0].[Payload], [j0].[PeriodEnd], [j0].[PeriodStart], [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId] + FROM [JoinOneToThreePayloadFullShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[ThreeId] = [e1].[Id] + ) AS [t] ON [e0].[Id] = [t].[OneId] +) AS [t0] ON [e].[Id] = [t0].[ThreeId] +ORDER BY [e].[Id], [t0].[OneId], [t0].[ThreeId], [t0].[Id], [t0].[OneId0], [t0].[ThreeId0], [t0].[Id0]"); + } + + public override async Task Filtered_include_skip_navigation_where(bool async) + { + await base.Filtered_include_skip_navigation_where(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t].[OneId], [t].[ThreeId], [t].[Payload], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[ThreeId], [j].[Payload], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0] + FROM [JoinOneToThreePayloadFullShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + WHERE [e0].[Id] < 10 +) AS [t] ON [e].[Id] = [t].[ThreeId] +ORDER BY [e].[Id], [t].[OneId], [t].[ThreeId], [t].[Id]"); + } + + public override async Task Filtered_include_skip_navigation_order_by(bool async) + { + await base.Filtered_include_skip_navigation_order_by(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t].[ThreeId], [t].[TwoId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[ThreeId], [j].[TwoId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e0].[ReferenceInverseId] + FROM [JoinTwoToThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[TwoId] = [e0].[Id] +) AS [t] ON [e].[Id] = [t].[ThreeId] +ORDER BY [e].[Id], [t].[Id], [t].[ThreeId], [t].[TwoId]"); + } + + public override async Task Filtered_include_skip_navigation_order_by_skip(bool async) + { + await base.Filtered_include_skip_navigation_order_by_skip(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t0].[LeftId], [t0].[RightId], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Id], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[ReferenceInverseId] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[LeftId], [t].[RightId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId] + FROM ( + SELECT [j].[LeftId], [j].[RightId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e0].[ReferenceInverseId], ROW_NUMBER() OVER(PARTITION BY [j].[LeftId] ORDER BY [e0].[Id]) AS [row] + FROM [JoinTwoSelfShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[RightId] = [e0].[Id] + ) AS [t] + WHERE 2 < [t].[row] +) AS [t0] ON [e].[Id] = [t0].[LeftId] +ORDER BY [e].[Id], [t0].[LeftId], [t0].[Id], [t0].[RightId]"); + } + + public override async Task Filtered_include_skip_navigation_order_by_take(bool async) + { + await base.Filtered_include_skip_navigation_order_by_take(async); + + AssertSql( + @"SELECT [e].[Key1], [e].[Key2], [e].[Key3], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [t0].[TwoId], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Id], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[ReferenceInverseId] +FROM [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[TwoId], [t].[CompositeId1], [t].[CompositeId2], [t].[CompositeId3], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId] + FROM ( + SELECT [j].[TwoId], [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e0].[ReferenceInverseId], ROW_NUMBER() OVER(PARTITION BY [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3] ORDER BY [e0].[Id]) AS [row] + FROM [JoinTwoToCompositeKeyShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[TwoId] = [e0].[Id] + ) AS [t] + WHERE [t].[row] <= 2 +) AS [t0] ON (([e].[Key1] = [t0].[CompositeId1]) AND ([e].[Key2] = [t0].[CompositeId2])) AND ([e].[Key3] = [t0].[CompositeId3]) +ORDER BY [e].[Key1], [e].[Key2], [e].[Key3], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[Id], [t0].[TwoId]"); + } + + public override async Task Filtered_include_skip_navigation_order_by_skip_take(bool async) + { + await base.Filtered_include_skip_navigation_order_by_skip_take(async); + + AssertSql( + @"SELECT [e].[Key1], [e].[Key2], [e].[Key3], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [t0].[Id], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[ThreeId], [t0].[Id0], [t0].[CollectionInverseId], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[ReferenceInverseId] +FROM [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [t].[Id], [t].[CompositeId1], [t].[CompositeId2], [t].[CompositeId3], [t].[PeriodEnd], [t].[PeriodStart], [t].[ThreeId], [t].[Id0], [t].[CollectionInverseId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId] + FROM ( + SELECT [j].[Id], [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[PeriodEnd], [j].[PeriodStart], [j].[ThreeId], [e0].[Id] AS [Id0], [e0].[CollectionInverseId], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e0].[ReferenceInverseId], ROW_NUMBER() OVER(PARTITION BY [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3] ORDER BY [e0].[Id]) AS [row] + FROM [JoinThreeToCompositeKeyFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[ThreeId] = [e0].[Id] + ) AS [t] + WHERE (1 < [t].[row]) AND ([t].[row] <= 3) +) AS [t0] ON (([e].[Key1] = [t0].[CompositeId1]) AND ([e].[Key2] = [t0].[CompositeId2])) AND ([e].[Key3] = [t0].[CompositeId3]) +ORDER BY [e].[Key1], [e].[Key2], [e].[Key3], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[Id0], [t0].[Id]"); + } + + public override async Task Filtered_then_include_skip_navigation_where(bool async) + { + await base.Filtered_then_include_skip_navigation_where(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Discriminator], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[Number], [e].[IsGreen], [t0].[EntityRootId], [t0].[EntityThreeId], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Id], [t0].[CollectionInverseId], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[ReferenceInverseId], [t0].[OneId], [t0].[ThreeId], [t0].[Payload], [t0].[PeriodEnd1], [t0].[PeriodStart1], [t0].[Id0], [t0].[Name0], [t0].[PeriodEnd00], [t0].[PeriodStart00] +FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [e0].[EntityRootId], [e0].[EntityThreeId], [e0].[PeriodEnd], [e0].[PeriodStart], [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId], [t].[OneId], [t].[ThreeId], [t].[Payload], [t].[PeriodEnd] AS [PeriodEnd1], [t].[PeriodStart] AS [PeriodStart1], [t].[Id] AS [Id0], [t].[Name] AS [Name0], [t].[PeriodEnd0] AS [PeriodEnd00], [t].[PeriodStart0] AS [PeriodStart00] + FROM [EntityRootEntityThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[EntityThreeId] = [e1].[Id] + LEFT JOIN ( + SELECT [j].[OneId], [j].[ThreeId], [j].[Payload], [j].[PeriodEnd], [j].[PeriodStart], [e2].[Id], [e2].[Name], [e2].[PeriodEnd] AS [PeriodEnd0], [e2].[PeriodStart] AS [PeriodStart0] + FROM [JoinOneToThreePayloadFullShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e2] ON [j].[OneId] = [e2].[Id] + WHERE [e2].[Id] < 10 + ) AS [t] ON [e1].[Id] = [t].[ThreeId] +) AS [t0] ON [e].[Id] = [t0].[EntityRootId] +ORDER BY [e].[Id], [t0].[EntityRootId], [t0].[EntityThreeId], [t0].[Id], [t0].[OneId], [t0].[ThreeId], [t0].[Id0]"); + } + + public override async Task Filtered_then_include_skip_navigation_order_by_skip_take(bool async) + { + await base.Filtered_then_include_skip_navigation_order_by_skip_take(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Discriminator], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[Number], [e].[IsGreen], [t1].[CompositeId1], [t1].[CompositeId2], [t1].[CompositeId3], [t1].[RootId], [t1].[PeriodEnd], [t1].[PeriodStart], [t1].[Key1], [t1].[Key2], [t1].[Key3], [t1].[Name], [t1].[PeriodEnd0], [t1].[PeriodStart0], [t1].[Id], [t1].[CompositeId10], [t1].[CompositeId20], [t1].[CompositeId30], [t1].[PeriodEnd1], [t1].[PeriodStart1], [t1].[ThreeId], [t1].[Id0], [t1].[CollectionInverseId], [t1].[Name0], [t1].[PeriodEnd00], [t1].[PeriodStart00], [t1].[ReferenceInverseId] +FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[RootId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Key1], [e0].[Key2], [e0].[Key3], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [t0].[Id], [t0].[CompositeId1] AS [CompositeId10], [t0].[CompositeId2] AS [CompositeId20], [t0].[CompositeId3] AS [CompositeId30], [t0].[PeriodEnd] AS [PeriodEnd1], [t0].[PeriodStart] AS [PeriodStart1], [t0].[ThreeId], [t0].[Id0], [t0].[CollectionInverseId], [t0].[Name] AS [Name0], [t0].[PeriodEnd0] AS [PeriodEnd00], [t0].[PeriodStart0] AS [PeriodStart00], [t0].[ReferenceInverseId] + FROM [JoinCompositeKeyToRootShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON (([j].[CompositeId1] = [e0].[Key1]) AND ([j].[CompositeId2] = [e0].[Key2])) AND ([j].[CompositeId3] = [e0].[Key3]) + LEFT JOIN ( + SELECT [t].[Id], [t].[CompositeId1], [t].[CompositeId2], [t].[CompositeId3], [t].[PeriodEnd], [t].[PeriodStart], [t].[ThreeId], [t].[Id0], [t].[CollectionInverseId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId] + FROM ( + SELECT [j0].[Id], [j0].[CompositeId1], [j0].[CompositeId2], [j0].[CompositeId3], [j0].[PeriodEnd], [j0].[PeriodStart], [j0].[ThreeId], [e1].[Id] AS [Id0], [e1].[CollectionInverseId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId], ROW_NUMBER() OVER(PARTITION BY [j0].[CompositeId1], [j0].[CompositeId2], [j0].[CompositeId3] ORDER BY [e1].[Id]) AS [row] + FROM [JoinThreeToCompositeKeyFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[ThreeId] = [e1].[Id] + ) AS [t] + WHERE (1 < [t].[row]) AND ([t].[row] <= 3) + ) AS [t0] ON (([e0].[Key1] = [t0].[CompositeId1]) AND ([e0].[Key2] = [t0].[CompositeId2])) AND ([e0].[Key3] = [t0].[CompositeId3]) +) AS [t1] ON [e].[Id] = [t1].[RootId] +ORDER BY [e].[Id], [t1].[CompositeId1], [t1].[CompositeId2], [t1].[CompositeId3], [t1].[RootId], [t1].[Key1], [t1].[Key2], [t1].[Key3], [t1].[CompositeId10], [t1].[CompositeId20], [t1].[CompositeId30], [t1].[Id0], [t1].[Id]"); + } + + public override async Task Filtered_include_skip_navigation_where_then_include_skip_navigation(bool async) + { + await base.Filtered_include_skip_navigation_where_then_include_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Discriminator], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[Number], [e].[IsGreen], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[LeafId], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Key1], [t0].[Key2], [t0].[Key3], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[TwoId], [t0].[CompositeId10], [t0].[CompositeId20], [t0].[CompositeId30], [t0].[PeriodEnd1], [t0].[PeriodStart1], [t0].[Id], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name0], [t0].[PeriodEnd00], [t0].[PeriodStart00], [t0].[ReferenceInverseId] +FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[CompositeId1], [j].[CompositeId2], [j].[CompositeId3], [j].[LeafId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Key1], [e0].[Key2], [e0].[Key3], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [t].[TwoId], [t].[CompositeId1] AS [CompositeId10], [t].[CompositeId2] AS [CompositeId20], [t].[CompositeId3] AS [CompositeId30], [t].[PeriodEnd] AS [PeriodEnd1], [t].[PeriodStart] AS [PeriodStart1], [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name] AS [Name0], [t].[PeriodEnd0] AS [PeriodEnd00], [t].[PeriodStart0] AS [PeriodStart00], [t].[ReferenceInverseId] + FROM [JoinCompositeKeyToLeaf] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityCompositeKeys] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON (([j].[CompositeId1] = [e0].[Key1]) AND ([j].[CompositeId2] = [e0].[Key2])) AND ([j].[CompositeId3] = [e0].[Key3]) + LEFT JOIN ( + SELECT [j0].[TwoId], [j0].[CompositeId1], [j0].[CompositeId2], [j0].[CompositeId3], [j0].[PeriodEnd], [j0].[PeriodStart], [e1].[Id], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId] + FROM [JoinTwoToCompositeKeyShared] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[TwoId] = [e1].[Id] + ) AS [t] ON (([e0].[Key1] = [t].[CompositeId1]) AND ([e0].[Key2] = [t].[CompositeId2])) AND ([e0].[Key3] = [t].[CompositeId3]) + WHERE [e0].[Key1] < 5 +) AS [t0] ON [e].[Id] = [t0].[LeafId] +WHERE [e].[Discriminator] = N'EntityLeaf' +ORDER BY [e].[Id], [t0].[CompositeId1], [t0].[CompositeId2], [t0].[CompositeId3], [t0].[LeafId], [t0].[Key1], [t0].[Key2], [t0].[Key3], [t0].[TwoId], [t0].[CompositeId10], [t0].[CompositeId20], [t0].[CompositeId30], [t0].[Id]"); + } + + public override async Task Filtered_include_skip_navigation_order_by_skip_take_then_include_skip_navigation_where(bool async) + { + await base.Filtered_include_skip_navigation_order_by_skip_take_then_include_skip_navigation_where(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [t1].[OneId], [t1].[TwoId], [t1].[JoinOneToTwoExtraId], [t1].[PeriodEnd], [t1].[PeriodStart], [t1].[Id], [t1].[CollectionInverseId], [t1].[ExtraId], [t1].[Name], [t1].[PeriodEnd0], [t1].[PeriodStart0], [t1].[ReferenceInverseId], [t1].[ThreeId], [t1].[TwoId0], [t1].[PeriodEnd1], [t1].[PeriodStart1], [t1].[Id0], [t1].[CollectionInverseId0], [t1].[Name0], [t1].[PeriodEnd00], [t1].[PeriodStart00], [t1].[ReferenceInverseId0] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +OUTER APPLY ( + SELECT [t].[OneId], [t].[TwoId], [t].[JoinOneToTwoExtraId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId], [t0].[ThreeId], [t0].[TwoId] AS [TwoId0], [t0].[PeriodEnd] AS [PeriodEnd1], [t0].[PeriodStart] AS [PeriodStart1], [t0].[Id] AS [Id0], [t0].[CollectionInverseId] AS [CollectionInverseId0], [t0].[Name] AS [Name0], [t0].[PeriodEnd0] AS [PeriodEnd00], [t0].[PeriodStart0] AS [PeriodStart00], [t0].[ReferenceInverseId] AS [ReferenceInverseId0] + FROM ( + SELECT [j].[OneId], [j].[TwoId], [j].[JoinOneToTwoExtraId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e0].[ReferenceInverseId] + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[TwoId] = [e0].[Id] + WHERE [e].[Id] = [j].[OneId] + ORDER BY [e0].[Id] + OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY + ) AS [t] + LEFT JOIN ( + SELECT [j0].[ThreeId], [j0].[TwoId], [j0].[PeriodEnd], [j0].[PeriodStart], [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId] + FROM [JoinTwoToThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[ThreeId] = [e1].[Id] + WHERE [e1].[Id] < 10 + ) AS [t0] ON [t].[Id] = [t0].[TwoId] +) AS [t1] +ORDER BY [e].[Id], [t1].[Id], [t1].[OneId], [t1].[TwoId], [t1].[ThreeId], [t1].[TwoId0], [t1].[Id0]"); + } + + public override async Task Filtered_include_skip_navigation_where_then_include_skip_navigation_order_by_skip_take(bool async) + { + await base.Filtered_include_skip_navigation_where_then_include_skip_navigation_order_by_skip_take(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [t1].[OneId], [t1].[TwoId], [t1].[JoinOneToTwoExtraId], [t1].[PeriodEnd], [t1].[PeriodStart], [t1].[Id], [t1].[CollectionInverseId], [t1].[ExtraId], [t1].[Name], [t1].[PeriodEnd0], [t1].[PeriodStart0], [t1].[ReferenceInverseId], [t1].[ThreeId], [t1].[TwoId0], [t1].[PeriodEnd1], [t1].[PeriodStart1], [t1].[Id0], [t1].[CollectionInverseId0], [t1].[Name0], [t1].[PeriodEnd00], [t1].[PeriodStart00], [t1].[ReferenceInverseId0] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[TwoId], [j].[JoinOneToTwoExtraId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e0].[ReferenceInverseId], [t0].[ThreeId], [t0].[TwoId] AS [TwoId0], [t0].[PeriodEnd] AS [PeriodEnd1], [t0].[PeriodStart] AS [PeriodStart1], [t0].[Id] AS [Id0], [t0].[CollectionInverseId] AS [CollectionInverseId0], [t0].[Name] AS [Name0], [t0].[PeriodEnd0] AS [PeriodEnd00], [t0].[PeriodStart0] AS [PeriodStart00], [t0].[ReferenceInverseId] AS [ReferenceInverseId0] + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[TwoId] = [e0].[Id] + LEFT JOIN ( + SELECT [t].[ThreeId], [t].[TwoId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[CollectionInverseId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId] + FROM ( + SELECT [j0].[ThreeId], [j0].[TwoId], [j0].[PeriodEnd], [j0].[PeriodStart], [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId], ROW_NUMBER() OVER(PARTITION BY [j0].[TwoId] ORDER BY [e1].[Id]) AS [row] + FROM [JoinTwoToThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[ThreeId] = [e1].[Id] + ) AS [t] + WHERE (1 < [t].[row]) AND ([t].[row] <= 3) + ) AS [t0] ON [e0].[Id] = [t0].[TwoId] + WHERE [e0].[Id] < 10 +) AS [t1] ON [e].[Id] = [t1].[OneId] +ORDER BY [e].[Id], [t1].[OneId], [t1].[TwoId], [t1].[Id], [t1].[TwoId0], [t1].[Id0], [t1].[ThreeId]"); + } + + public override async Task Filter_include_on_skip_navigation_combined(bool async) + { + await base.Filter_include_on_skip_navigation_combined(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[ExtraId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t].[OneId], [t].[TwoId], [t].[JoinOneToTwoExtraId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[Id0], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name0], [t].[PeriodEnd1], [t].[PeriodStart1], [t].[ReferenceInverseId], [t].[Id1], [t].[CollectionInverseId0], [t].[ExtraId0], [t].[Name1], [t].[PeriodEnd2], [t].[PeriodStart2], [t].[ReferenceInverseId0] +FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[TwoId], [j].[JoinOneToTwoExtraId], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [e1].[Id] AS [Id0], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name] AS [Name0], [e1].[PeriodEnd] AS [PeriodEnd1], [e1].[PeriodStart] AS [PeriodStart1], [e1].[ReferenceInverseId], [e2].[Id] AS [Id1], [e2].[CollectionInverseId] AS [CollectionInverseId0], [e2].[ExtraId] AS [ExtraId0], [e2].[Name] AS [Name1], [e2].[PeriodEnd] AS [PeriodEnd2], [e2].[PeriodStart] AS [PeriodStart2], [e2].[ReferenceInverseId] AS [ReferenceInverseId0] + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + LEFT JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [e0].[Id] = [e1].[ReferenceInverseId] + LEFT JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e2] ON [e0].[Id] = [e2].[CollectionInverseId] + WHERE [e0].[Id] < 10 +) AS [t] ON [e].[Id] = [t].[TwoId] +ORDER BY [e].[Id], [t].[OneId], [t].[TwoId], [t].[Id], [t].[Id0], [t].[Id1]"); + } + + public override async Task Filter_include_on_skip_navigation_combined_with_filtered_then_includes(bool async) + { + await base.Filter_include_on_skip_navigation_combined_with_filtered_then_includes(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t3].[OneId], [t3].[ThreeId], [t3].[Payload], [t3].[PeriodEnd], [t3].[PeriodStart], [t3].[Id], [t3].[Name], [t3].[PeriodEnd0], [t3].[PeriodStart0], [t3].[OneId0], [t3].[TwoId], [t3].[JoinOneToTwoExtraId], [t3].[PeriodEnd1], [t3].[PeriodStart1], [t3].[Id0], [t3].[CollectionInverseId], [t3].[ExtraId], [t3].[Name0], [t3].[PeriodEnd00], [t3].[PeriodStart00], [t3].[ReferenceInverseId], [t3].[EntityBranchId], [t3].[EntityOneId], [t3].[PeriodEnd2], [t3].[PeriodStart2], [t3].[Id1], [t3].[Discriminator], [t3].[Name1], [t3].[PeriodEnd01], [t3].[PeriodStart01], [t3].[Number], [t3].[IsGreen] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[ThreeId], [j].[Payload], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [t0].[OneId] AS [OneId0], [t0].[TwoId], [t0].[JoinOneToTwoExtraId], [t0].[PeriodEnd] AS [PeriodEnd1], [t0].[PeriodStart] AS [PeriodStart1], [t0].[Id] AS [Id0], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name] AS [Name0], [t0].[PeriodEnd0] AS [PeriodEnd00], [t0].[PeriodStart0] AS [PeriodStart00], [t0].[ReferenceInverseId], [t1].[EntityBranchId], [t1].[EntityOneId], [t1].[PeriodEnd] AS [PeriodEnd2], [t1].[PeriodStart] AS [PeriodStart2], [t1].[Id] AS [Id1], [t1].[Discriminator], [t1].[Name] AS [Name1], [t1].[PeriodEnd0] AS [PeriodEnd01], [t1].[PeriodStart0] AS [PeriodStart01], [t1].[Number], [t1].[IsGreen] + FROM [JoinOneToThreePayloadFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + LEFT JOIN ( + SELECT [t].[OneId], [t].[TwoId], [t].[JoinOneToTwoExtraId], [t].[PeriodEnd], [t].[PeriodStart], [t].[Id], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name], [t].[PeriodEnd0], [t].[PeriodStart0], [t].[ReferenceInverseId] + FROM ( + SELECT [j0].[OneId], [j0].[TwoId], [j0].[JoinOneToTwoExtraId], [j0].[PeriodEnd], [j0].[PeriodStart], [e1].[Id], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId], ROW_NUMBER() OVER(PARTITION BY [j0].[OneId] ORDER BY [e1].[Id]) AS [row] + FROM [JoinOneToTwo] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j0] + INNER JOIN [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j0].[TwoId] = [e1].[Id] + ) AS [t] + WHERE (1 < [t].[row]) AND ([t].[row] <= 3) + ) AS [t0] ON [e0].[Id] = [t0].[OneId] + LEFT JOIN ( + SELECT [j1].[EntityBranchId], [j1].[EntityOneId], [j1].[PeriodEnd], [j1].[PeriodStart], [t2].[Id], [t2].[Discriminator], [t2].[Name], [t2].[PeriodEnd] AS [PeriodEnd0], [t2].[PeriodStart] AS [PeriodStart0], [t2].[Number], [t2].[IsGreen] + FROM [JoinOneToBranch] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j1] + INNER JOIN ( + SELECT [e2].[Id], [e2].[Discriminator], [e2].[Name], [e2].[PeriodEnd], [e2].[PeriodStart], [e2].[Number], [e2].[IsGreen] + FROM [EntityRoots] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e2] + WHERE [e2].[Discriminator] IN (N'EntityBranch', N'EntityLeaf') + ) AS [t2] ON [j1].[EntityBranchId] = [t2].[Id] + WHERE [t2].[Id] < 20 + ) AS [t1] ON [e0].[Id] = [t1].[EntityOneId] + WHERE [e0].[Id] < 10 +) AS [t3] ON [e].[Id] = [t3].[ThreeId] +ORDER BY [e].[Id], [t3].[OneId], [t3].[ThreeId], [t3].[Id], [t3].[OneId0], [t3].[Id0], [t3].[TwoId], [t3].[EntityBranchId], [t3].[EntityOneId], [t3].[Id1]"); + } + + public override async Task Filtered_include_on_skip_navigation_then_filtered_include_on_navigation(bool async) + { + await base.Filtered_include_on_skip_navigation_then_filtered_include_on_navigation(async); + + AssertSql( + @"SELECT [e].[Id], [e].[CollectionInverseId], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [e].[ReferenceInverseId], [t0].[OneId], [t0].[ThreeId], [t0].[Payload], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[Id], [t0].[Name], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[Id0], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name0], [t0].[PeriodEnd1], [t0].[PeriodStart1], [t0].[ReferenceInverseId] +FROM [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [j].[OneId], [j].[ThreeId], [j].[Payload], [j].[PeriodEnd], [j].[PeriodStart], [e0].[Id], [e0].[Name], [e0].[PeriodEnd] AS [PeriodEnd0], [e0].[PeriodStart] AS [PeriodStart0], [t].[Id] AS [Id0], [t].[CollectionInverseId], [t].[ExtraId], [t].[Name] AS [Name0], [t].[PeriodEnd] AS [PeriodEnd1], [t].[PeriodStart] AS [PeriodStart1], [t].[ReferenceInverseId] + FROM [JoinOneToThreePayloadFull] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] ON [j].[OneId] = [e0].[Id] + LEFT JOIN ( + SELECT [e1].[Id], [e1].[CollectionInverseId], [e1].[ExtraId], [e1].[Name], [e1].[PeriodEnd], [e1].[PeriodStart], [e1].[ReferenceInverseId] + FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] + WHERE [e1].[Id] < 5 + ) AS [t] ON [e0].[Id] = [t].[CollectionInverseId] + WHERE [e0].[Id] > 15 +) AS [t0] ON [e].[Id] = [t0].[ThreeId] +ORDER BY [e].[Id], [t0].[OneId], [t0].[ThreeId], [t0].[Id], [t0].[Id0]"); + } + + public override async Task Filtered_include_on_navigation_then_filtered_include_on_skip_navigation(bool async) + { + await base.Filtered_include_on_navigation_then_filtered_include_on_skip_navigation(async); + + AssertSql( + @"SELECT [e].[Id], [e].[Name], [e].[PeriodEnd], [e].[PeriodStart], [t0].[Id], [t0].[CollectionInverseId], [t0].[ExtraId], [t0].[Name], [t0].[PeriodEnd], [t0].[PeriodStart], [t0].[ReferenceInverseId], [t0].[ThreeId], [t0].[TwoId], [t0].[PeriodEnd0], [t0].[PeriodStart0], [t0].[Id0], [t0].[CollectionInverseId0], [t0].[Name0], [t0].[PeriodEnd00], [t0].[PeriodStart00], [t0].[ReferenceInverseId0] +FROM [EntityOnes] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e] +LEFT JOIN ( + SELECT [e0].[Id], [e0].[CollectionInverseId], [e0].[ExtraId], [e0].[Name], [e0].[PeriodEnd], [e0].[PeriodStart], [e0].[ReferenceInverseId], [t].[ThreeId], [t].[TwoId], [t].[PeriodEnd] AS [PeriodEnd0], [t].[PeriodStart] AS [PeriodStart0], [t].[Id] AS [Id0], [t].[CollectionInverseId] AS [CollectionInverseId0], [t].[Name] AS [Name0], [t].[PeriodEnd0] AS [PeriodEnd00], [t].[PeriodStart0] AS [PeriodStart00], [t].[ReferenceInverseId] AS [ReferenceInverseId0] + FROM [EntityTwos] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e0] + LEFT JOIN ( + SELECT [j].[ThreeId], [j].[TwoId], [j].[PeriodEnd], [j].[PeriodStart], [e1].[Id], [e1].[CollectionInverseId], [e1].[Name], [e1].[PeriodEnd] AS [PeriodEnd0], [e1].[PeriodStart] AS [PeriodStart0], [e1].[ReferenceInverseId] + FROM [JoinTwoToThree] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [j] + INNER JOIN [EntityThrees] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [e1] ON [j].[ThreeId] = [e1].[Id] + WHERE [e1].[Id] < 5 + ) AS [t] ON [e0].[Id] = [t].[TwoId] + WHERE [e0].[Id] > 15 +) AS [t0] ON [e].[Id] = [t0].[CollectionInverseId] +ORDER BY [e].[Id], [t0].[Id], [t0].[ThreeId], [t0].[TwoId], [t0].[Id0]"); + } + + public override async Task Includes_accessed_via_different_path_are_merged(bool async) + { + await base.Includes_accessed_via_different_path_are_merged(async); + + AssertSql( + @""); + } + + public override async Task Filered_includes_accessed_via_different_path_are_merged(bool async) + { + await base.Filered_includes_accessed_via_different_path_are_merged(async); + + AssertSql( + @""); + } + + public override async Task Throws_when_different_filtered_then_include_via_different_paths(bool async) + { + await base.Throws_when_different_filtered_then_include_via_different_paths(async); + + AssertSql( + @""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalPointInTimeQueryRewriter.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalPointInTimeQueryRewriter.cs new file mode 100644 index 00000000000..6ff8bd04028 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalPointInTimeQueryRewriter.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class TemporalPointInTimeQueryRewriter : ExpressionVisitor + { + private static readonly MethodInfo _setMethodInfo + = typeof(ISetSource).GetMethod(nameof(ISetSource.Set)); + + private static readonly MethodInfo _asOfMethodInfo + = typeof(SqlServerQueryableExtensions).GetMethod(nameof(SqlServerQueryableExtensions.TemporalAsOf)); + + private readonly DateTime _pointInTime; + + // TODO: need model instead + private readonly List _temporalEntityTypes; + + public TemporalPointInTimeQueryRewriter(DateTime pointInTime, List temporalEntityTypes) + { + _pointInTime = pointInTime; + _temporalEntityTypes = temporalEntityTypes; + } + + protected override Expression VisitExtension(Expression extensionExpression) + { + if (extensionExpression is QueryRootExpression queryRootExpression + && queryRootExpression.EntityType.GetRootType().IsTemporal()) + { + return new TemporalAsOfQueryRootExpression( + queryRootExpression.QueryProvider, + queryRootExpression.EntityType, + _pointInTime); + } + + return base.VisitExtension(extensionExpression); + } + + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + // TODO: issue #25236 - also match named sets + // in case we want to reuse this on queries that are not using AssertQuery infra + if (methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.GetGenericMethodDefinition() == _setMethodInfo) + { + var entityType = methodCallExpression.Method.GetGenericArguments()[0]; + if (_temporalEntityTypes.Contains(entityType)) + { + var method = _asOfMethodInfo.MakeGenericMethod(methodCallExpression.Method.GetGenericArguments()[0]); + + // temporal methods are defined on DBSet so we need to hard cast here. + // This rewrite is only done for actual queries (and not expected), so the cast is safe to do + var dbSetType = typeof(DbSet<>).MakeGenericType(entityType); + + return Expression.Call( + method, + Expression.Convert( + methodCallExpression, + dbSetType), + Expression.Constant(_pointInTime)); + } + } + + return base.VisitMethodCall(methodCallExpression); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs index 71d2ac81819..be68140a05f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs @@ -16,5 +16,6 @@ public enum SqlServerCondition IsNotCI = 1 << 5, SupportsFullTextSearch = 1 << 6, SupportsOnlineIndexes = 1 << 7, + SupportsTemporalTablesCascadeDelete = 1 << 8, } } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs index 903db7cb539..eb19fa0f88d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs @@ -65,6 +65,11 @@ public ValueTask IsMetAsync() isMet &= TestEnvironment.IsOnlineIndexingSupported; } + if (Conditions.HasFlag(SqlServerCondition.SupportsTemporalTablesCascadeDelete)) + { + isMet &= TestEnvironment.IsTemporalTablesCascadeDeleteSupported; + } + return new ValueTask(isMet); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs index 35fcc9d8d42..4ae1d56ae6d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs @@ -37,6 +37,8 @@ public static class TestEnvironment private static bool? _supportsMemoryOptimizedTables; + private static bool? _supportsTemporalTablesCascadeDelete; + private static byte? _productMajorVersion; private static int? _engineEdition; @@ -204,6 +206,36 @@ public static bool IsMemoryOptimizedTablesSupported } } + public static bool IsTemporalTablesCascadeDeleteSupported + { + get + { + if (!IsConfigured) + { + return false; + } + + if (_supportsTemporalTablesCascadeDelete.HasValue) + { + return _supportsTemporalTablesCascadeDelete.Value; + } + + try + { + _engineEdition = GetEngineEdition(); + _productMajorVersion = GetProductMajorVersion(); + + _supportsTemporalTablesCascadeDelete = (_productMajorVersion >= 14/* && _engineEdition != 6*/) || IsSqlAzure; + } + catch (PlatformNotSupportedException) + { + _supportsTemporalTablesCascadeDelete = false; + } + + return _supportsTemporalTablesCascadeDelete.Value; + } + } + public static string ElasticPoolName { get; } = Config["ElasticPoolName"]; public static bool? GetFlag(string key)