Skip to content

Commit

Permalink
query part
Browse files Browse the repository at this point in the history
- query apis for temporal operations
- query root creator service that can now construct query root expressions in nav expansion

tests for now are simply converted exisiting tests
- used the original model & data
- map entities to temporal in the model configuration
- modify the data (remove or change values),
- manually change the history table to make history deterministic (rather than based on current date),
- added visitor to inject temporal operation to every query, which "time travels" to before the modifications above were made, so we should still get the same results as non-temporal & not modified data
  • Loading branch information
maumar committed Jul 13, 2021
1 parent 122442a commit 4b25a88
Show file tree
Hide file tree
Showing 44 changed files with 3,779 additions and 43 deletions.
211 changes: 211 additions & 0 deletions 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
{
/// <summary>
/// Sql Server database specific extension methods for LINQ queries.
/// </summary>
public static class SqlServerQueryableExtensions
{
/// <summary>
/// <para>
/// Applies temporal 'AsOf' operation on the given DbSet, which only returns elements that were present in the database at a given point in time.
/// </para>
/// <para>
/// Temporal queries are always set as 'NoTracking'.
/// </para>
/// </summary>
/// <param name="source">Source DbSet on which the temporal operation is applied.</param>
/// <param name="pointInTime"><see cref="DateTime" /> representing a point in time for which the results should be returned.</param>
/// <returns> An <see cref="IQueryable{T}" /> representing the entities at a given point in time.</returns>
public static IQueryable<TEntity> TemporalAsOf<TEntity>(
this DbSet<TEntity> source,
DateTime pointInTime)
where TEntity : class
{
Check.NotNull(source, nameof(source));

var queryableSource = (IQueryable)source;

return queryableSource.Provider.CreateQuery<TEntity>(
GenerateTemporalAsOfQueryRoot<TEntity>(
queryableSource,
pointInTime)).AsNoTracking();
}

/// <summary>
/// <para>
/// Applies temporal 'FromTo' operation on the given DbSet, which only returns elements that were present in the database between two points in time.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Temporal queries are always set as 'NoTracking'.
/// </para>
/// </summary>
/// <param name="source">Source DbSet on which the temporal operation is applied.</param>
/// <param name="from">Point in time representing the start of the period for which results should be returned.</param>
/// <param name="to">Point in time representing the end of the period for which results should be returned.</param>
/// <returns> An <see cref="IQueryable{T}" /> representing the entities present in a given time range.</returns>
public static IQueryable<TEntity> TemporalFromTo<TEntity>(
this DbSet<TEntity> source,
DateTime from,
DateTime to)
where TEntity : class
{
Check.NotNull(source, nameof(source));

var queryableSource = (IQueryable)source;

return queryableSource.Provider.CreateQuery<TEntity>(
GenerateRangeTemporalQueryRoot<TEntity>(
queryableSource,
from,
to,
TemporalOperationType.FromTo)).AsNoTracking();
}

/// <summary>
/// <para>
/// Applies temporal 'Between' operation on the given DbSet, which only returns elements that were present in the database between two points in time.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Temporal queries are always set as 'NoTracking'.
/// </para>
/// </summary>
/// <param name="source">Source DbSet on which the temporal operation is applied.</param>
/// <param name="from">Point in time representing the start of the period for which results should be returned.</param>
/// <param name="to">Point in time representing the end of the period for which results should be returned.</param>
/// <returns> An <see cref="IQueryable{T}" /> representing the entities present in a given time range.</returns>
public static IQueryable<TEntity> TemporalBetween<TEntity>(
this IQueryable<TEntity> source,
DateTime from,
DateTime to)
where TEntity : class
{
Check.NotNull(source, nameof(source));

var queryableSource = (IQueryable)source;

return queryableSource.Provider.CreateQuery<TEntity>(
GenerateRangeTemporalQueryRoot<TEntity>(
queryableSource,
from,
to,
TemporalOperationType.Between)).AsNoTracking();
}

/// <summary>
/// <para>
/// Applies temporal 'ContainedIn' operation on the given DbSet, which only returns elements that were present in the database between two points in time.
/// </para>
/// <para>
/// Elements that were created at the starting point as well as elements that were removed at the end point are included in the results.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Temporal queries are always set as 'NoTracking'.
/// </para>
/// </summary>
/// <param name="source">Source DbSet on which the temporal operation is applied.</param>
/// <param name="from">Point in time representing the start of the period for which results should be returned.</param>
/// <param name="to">Point in time representing the end of the period for which results should be returned.</param>
/// <returns> An <see cref="IQueryable{T}" /> representing the entities present in a given time range.</returns>
public static IQueryable<TEntity> TemporalContainedIn<TEntity>(
this DbSet<TEntity> source,
DateTime from,
DateTime to)
where TEntity : class
{
Check.NotNull(source, nameof(source));

var queryableSource = (IQueryable)source;

return queryableSource.Provider.CreateQuery<TEntity>(
GenerateRangeTemporalQueryRoot<TEntity>(
queryableSource,
from,
to,
TemporalOperationType.ContainedIn)).AsNoTracking();
}

/// <summary>
/// <para>
/// Applies temporal 'All' operation on the given DbSet, which returns all historical versions of the entities as well as their current state.
/// </para>
/// <para>
/// Temporal queries are always set as 'NoTracking'.
/// </para>
/// </summary>
/// <param name="source">Source DbSet on which the temporal operation is applied.</param>
/// <returns> An <see cref="IQueryable{T}" /> representing the entities and their historical versions.</returns>
public static IQueryable<TEntity> TemporalAll<TEntity>(
this DbSet<TEntity> 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<TEntity>(temporalQueryRootExpression)
.AsNoTracking();
}

private static Expression GenerateTemporalAsOfQueryRoot<TEntity>(
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<TEntity>(
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);
}
}
}
Expand Up @@ -79,6 +79,8 @@ public static IServiceCollection AddEntityFrameworkSqlServer(this IServiceCollec
.TryAdd<IQuerySqlGeneratorFactory, SqlServerQuerySqlGeneratorFactory>()
.TryAdd<IRelationalSqlTranslatingExpressionVisitorFactory, SqlServerSqlTranslatingExpressionVisitorFactory>()
.TryAdd<IRelationalParameterBasedSqlProcessorFactory, SqlServerParameterBasedSqlProcessorFactory>()
.TryAdd<IQueryRootCreator, SqlServerQueryRootCreator>()
.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, SqlServerQueryableMethodTranslatingExpressionVisitorFactory>()
.TryAddProviderSpecificServices(
b => b
.TryAddSingleton<ISqlServerValueGeneratorCache, SqlServerValueGeneratorCache>()
Expand Down
Expand Up @@ -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);
Expand Down Expand Up @@ -110,6 +111,8 @@ public override ConventionSet CreateConventionSet()
conventionSet.ModelFinalizedConventions,
(RuntimeModelConvention)new SqlServerRuntimeModelConvention(Dependencies, RelationalDependencies));

conventionSet.SkipNavigationForeignKeyChangedConventions.Add(sqlServerTemporalConvention);

return conventionSet;
}

Expand Down
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
/// <summary>
/// A convention that manipulates temporal settings for an entity mapped to a temporal table.
/// </summary>
public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConvention
public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConvention, ISkipNavigationForeignKeyChangedConvention
{
private const string PeriodStartDefaultName = "PeriodStart";
private const string PeriodEndDefaultName = "PeriodEnd";
Expand Down Expand Up @@ -81,5 +81,24 @@ public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConventio
}
}
}

/// <inheritdoc />
public virtual void ProcessSkipNavigationForeignKeyChanged(
IConventionSkipNavigationBuilder skipNavigationBuilder,
IConventionForeignKey? foreignKey,
IConventionForeignKey? oldForeignKey,
IConventionContext<IConventionForeignKey> 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);
}
}
}
}
24 changes: 24 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Expand Up @@ -319,4 +319,16 @@
<value>Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified.</value>
</data>

<data name="TemporalNavigationExpansionBetweenTemporalAndNonTemporal" xml:space="preserve">
<value>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.</value>
</data>

<data name="TemporalNavigationExpansionOnlySupportedForAsOf" xml:space="preserve">
<value>Navigation expansion is only supported for '{operationName}' temporal operation. For other operations use join manually.</value>
</data>

<data name="TemporalSetOperationOnMismatchedSources" xml:space="preserve">
<value>Set operation can't be applied on entity '{entityType}' because temporal operations on both arguments don't match.</value>
</data>

</root>
Expand Up @@ -53,5 +53,21 @@ public class SqlServerParameterBasedSqlProcessor : RelationalParameterBasedSqlPr
return (SelectExpression)new SearchConditionConvertingExpressionVisitor(Dependencies.SqlExpressionFactory)
.Visit(optimizedSelectExpression);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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.
/// </summary>
protected override SelectExpression ProcessSqlNullability(
SelectExpression selectExpression,
IReadOnlyDictionary<string, object?> parametersValues, out bool canCache)
{
Check.NotNull(selectExpression, nameof(selectExpression));
Check.NotNull(parametersValues, nameof(parametersValues));

return new SqlServerSqlNullabilityProcessor(Dependencies, UseRelationalNulls).Process(selectExpression, parametersValues, out canCache);
}
}
}

0 comments on commit 4b25a88

Please sign in to comment.