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 7, 2021
1 parent 106ac1a commit 6383d70
Show file tree
Hide file tree
Showing 43 changed files with 3,094 additions and 56 deletions.
211 changes: 211 additions & 0 deletions src/EFCore.SqlServer/Extensions/SqlServerQueryableExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Conventions;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -64,9 +65,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 @@ -106,10 +108,22 @@ public override ConventionSet CreateConventionSet()
(SharedTableConvention)new SqlServerSharedTableConvention(Dependencies, RelationalDependencies));
conventionSet.ModelFinalizingConventions.Add(new SqlServerDbFunctionConvention(Dependencies, RelationalDependencies));

//var manyToManyJoinEntityTypeConvention = new SqlServerManyToManyJoinEntityTypeConvention(Dependencies);
//ReplaceConvention(
// conventionSet.SkipNavigationAddedConventions,
// (ManyToManyJoinEntityTypeConvention)manyToManyJoinEntityTypeConvention);

//ReplaceConvention(
// conventionSet.SkipNavigationInverseChangedConventions,
// (ManyToManyJoinEntityTypeConvention)manyToManyJoinEntityTypeConvention);

ReplaceConvention(
conventionSet.ModelFinalizedConventions,
(RuntimeModelConvention)new SqlServerRuntimeModelConvention(Dependencies, RelationalDependencies));

//conventionSet.ModelFinalizingConventions.Add(sqlServerTemporalConvention);
conventionSet.SkipNavigationForeignKeyChangedConventions.Add(sqlServerTemporalConvention);

return conventionSet;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;

namespace Microsoft.EntityFrameworkCore.SqlServer.Metadata.Conventions
{
///// <inheritdoc />
//public class SqlServerManyToManyJoinEntityTypeConvention : ManyToManyJoinEntityTypeConvention
//{
// /// <summary>
// /// Creates a new instance of <see cref="SqlServerManyToManyJoinEntityTypeConvention" />.
// /// </summary>
// /// <param name="dependencies"> Parameter object containing dependencies for this convention. </param>
// public SqlServerManyToManyJoinEntityTypeConvention(ProviderConventionSetBuilderDependencies dependencies)
// : base(dependencies)
// {
// }

// /// <inheritdoc />
// public override void ProcessSkipNavigationAdded(
// IConventionSkipNavigationBuilder skipNavigationBuilder,
// IConventionContext<IConventionSkipNavigationBuilder> context)
// {
// base.ProcessSkipNavigationAdded(skipNavigationBuilder, context);
// AddTemporalInformation(skipNavigationBuilder);
// }

// /// <inheritdoc />
// public override void ProcessSkipNavigationInverseChanged(
// IConventionSkipNavigationBuilder skipNavigationBuilder,
// IConventionSkipNavigation? inverse,
// IConventionSkipNavigation? oldInverse,
// IConventionContext<IConventionSkipNavigation> context)
// {
// base.ProcessSkipNavigationInverseChanged(skipNavigationBuilder, inverse, oldInverse, context);
// AddTemporalInformation(skipNavigationBuilder);
// }

// private void AddTemporalInformation(IConventionSkipNavigationBuilder skipNavigationBuilder)
// {
// var skipNavigation = skipNavigationBuilder.Metadata;

// if (!skipNavigation.IsCollection)
// {
// return;
// }

// var inverseSkipNavigation = skipNavigation.Inverse;
// if (inverseSkipNavigation == null
// || !inverseSkipNavigation.IsCollection)
// {
// return;
// }

// var declaringEntityType = skipNavigation.DeclaringEntityType;
// var inverseEntityType = inverseSkipNavigation.DeclaringEntityType;

// if (declaringEntityType.IsTemporal()
// && inverseEntityType.IsTemporal())
// {
// var model = declaringEntityType.Model;

// var joinEntityTypeName = declaringEntityType.ShortName();
// var inverseName = inverseEntityType.ShortName();

// joinEntityTypeName = StringComparer.Ordinal.Compare(joinEntityTypeName, inverseName) < 0
// ? joinEntityTypeName + inverseName
// : inverseName + joinEntityTypeName;

// var joinEntityType = model.FindEntityType(joinEntityTypeName);
// if (joinEntityType != null
// && !joinEntityType.IsTemporal())
// {
// joinEntityType.SetIsTemporal(true);
// }
// }
// }
//}
}
Loading

0 comments on commit 6383d70

Please sign in to comment.