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)