Skip to content

Commit

Permalink
Adding support for owned types mapped to same table as well as differ…
Browse files Browse the repository at this point in the history
…ent tables

Fix to #26451 - Temporal Table: Owned Entities support
Fix to #26469 - Query: enable temporal tables for table splitting scenarios
Fix to #26705 - Query: model building for temporal table with explicitly defined period columns could fail if conventions are executed in a delayed fashion

Just like any other nav expansion, this only works for AsOf operation.

Added extensibility point to SharedTypeEntityExpandingExpressionVisitor so that we can create temporal table expressions representing owned entities, if their owner is also a temporal table expression.
We also need to match TableExpression information with ITable metadata, for provider specific table-like expressions.

Relaxed validation for table splitting when the table is temporal - it's now allowed as long as all (base type) entities mapped to this table have congruent period and history table mappings.

Column period property is now created explicitly in the temporal table builder rather than relying on conventions to avoid scenario when conventions are delayed and the period property they are supposed to create is not ready by the time we need it.
  • Loading branch information
maumar committed Nov 16, 2021
1 parent fb61eb7 commit f919bec
Show file tree
Hide file tree
Showing 18 changed files with 2,488 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public static readonly IDictionary<Type, ServiceCharacteristics> RelationalServi
{ typeof(ISqlExpressionFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IRelationalQueryStringFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IRelationalParameterBasedSqlProcessorFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IRelationalSharedTypeEntityExpansionHelper), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IMigrationsModelDiffer), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IMigrationsSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Scoped) },
{ typeof(IMigrator), new ServiceCharacteristics(ServiceLifetime.Scoped) },
Expand Down Expand Up @@ -190,6 +191,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd<IRelationalParameterBasedSqlProcessorFactory, RelationalParameterBasedSqlProcessorFactory>();
TryAdd<IRelationalQueryStringFactory, RelationalQueryStringFactory>();
TryAdd<IQueryCompilationContextFactory, RelationalQueryCompilationContextFactory>();
TryAdd<IRelationalSharedTypeEntityExpansionHelper, RelationalSharedTypeEntityExpansionHelper>();

ServiceCollectionMap.GetInfrastructure()
.AddDependencySingleton<RelationalSqlGenerationHelperDependencies>()
Expand Down Expand Up @@ -225,7 +227,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
.AddDependencyScoped<RelationalConnectionDependencies>()
.AddDependencyScoped<RelationalDatabaseDependencies>()
.AddDependencyScoped<RelationalQueryContextDependencies>()
.AddDependencyScoped<RelationalQueryCompilationContextDependencies>();
.AddDependencyScoped<RelationalQueryCompilationContextDependencies>()
.AddDependencyScoped<RelationalSharedTypeEntityExpansionHelperDependencies>();

return base.TryAddCoreServices();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.EntityFrameworkCore.Query
{
/// <summary>
/// Service which helps with various aspects of shared type entity expansion extensibility for relational providrers.
/// </summary>
/// <para>
/// The service lifetime is <see cref="ServiceLifetime.Scoped" />. This means that each
/// <see cref="DbContext" /> instance will use its own instance of this service.
/// The implementation may depend on other services registered with any lifetime.
/// The implementation does not need to be thread-safe.
/// </para>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-providers">Implementation of database providers and extensions</see>
/// and <see href="https://aka.ms/efcore-how-queries-work">How EF Core queries work</see> for more information.
/// </remarks>
public interface IRelationalSharedTypeEntityExpansionHelper
{
/// <summary>
/// Creates a SelectExpression representing owned type.
/// </summary>
public SelectExpression CreateInnerSelectExpression(
TableExpressionBase sourceTable,
IEntityType targetEntityType);

/// <summary>
/// Returns true if the given table expression matches table metadata, false otherwise.
/// </summary>
public bool TableMatchesMetadata(
TableExpressionBase tableExpression,
ITableBase tableMetadata);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ public RelationalQueryableMethodTranslatingExpressionVisitor(
_queryCompilationContext = queryCompilationContext;
_sqlTranslator = relationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(queryCompilationContext, this);
_sharedTypeEntityExpandingExpressionVisitor =
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory);
new SharedTypeEntityExpandingExpressionVisitor(
_sqlTranslator,
sqlExpressionFactory,
relationalDependencies.RelationalSharedTypeEntityExpansionHelper);
_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
_sqlExpressionFactory = sqlExpressionFactory;
_subquery = false;
Expand All @@ -70,7 +73,11 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor(
_sqlTranslator = RelationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(
parentVisitor._queryCompilationContext, parentVisitor);
_sharedTypeEntityExpandingExpressionVisitor =
new SharedTypeEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory);
new SharedTypeEntityExpandingExpressionVisitor(
_sqlTranslator,
parentVisitor._sqlExpressionFactory,
RelationalDependencies.RelationalSharedTypeEntityExpansionHelper);

_projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator);
_sqlExpressionFactory = parentVisitor._sqlExpressionFactory;
_subquery = true;
Expand Down Expand Up @@ -997,15 +1004,18 @@ private static readonly MethodInfo _objectEqualsMethodInfo

private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator;
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalSharedTypeEntityExpansionHelper _sharedTypeEntityExpansionHelper;

private SelectExpression _selectExpression;

public SharedTypeEntityExpandingExpressionVisitor(
RelationalSqlTranslatingExpressionVisitor sqlTranslator,
ISqlExpressionFactory sqlExpressionFactory)
ISqlExpressionFactory sqlExpressionFactory,
IRelationalSharedTypeEntityExpansionHelper sharedTypeEntityExpansionHelper)
{
_sqlTranslator = sqlTranslator;
_sqlExpressionFactory = sqlExpressionFactory;
_sharedTypeEntityExpansionHelper = sharedTypeEntityExpansionHelper;
_selectExpression = null!;
}

Expand Down Expand Up @@ -1087,11 +1097,21 @@ protected override Expression VisitExtension(Expression extensionExpression)
return null;
}

var entityProjectionExpression = (EntityProjectionExpression)
(entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression
? _selectExpression.GetProjection(projectionBindingExpression)
: entityShaperExpression.ValueBufferExpression);

var foreignKey = navigation.ForeignKey;
if (navigation.IsCollection)
{
var innerShapedQuery = CreateShapedQueryExpression(
targetEntityType, _sqlExpressionFactory.Select(targetEntityType));
var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
entityProjectionExpression,
navigation,
foreignKey,
targetEntityType);

var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression);

var makeNullable = foreignKey.PrincipalKey.Properties
.Concat(foreignKey.Properties)
Expand Down Expand Up @@ -1139,11 +1159,6 @@ outerKey is NewArrayExpression newArrayExpression
Expression.Quote(correlationPredicate));
}

var entityProjectionExpression = (EntityProjectionExpression)
(entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression
? _selectExpression.GetProjection(projectionBindingExpression)
: entityShaperExpression.ValueBufferExpression);

var innerShaper = entityProjectionExpression.BindNavigation(navigation);
if (innerShaper == null)
{
Expand Down Expand Up @@ -1171,7 +1186,12 @@ outerKey is NewArrayExpression newArrayExpression
&& navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType));

var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression(
targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable);
targetEntityType,
table,
identifyingColumn.Name,
identifyingColumn.Table,
_sharedTypeEntityExpansionHelper,
principalNullable);

if (entityProjection != null)
{
Expand All @@ -1186,7 +1206,13 @@ outerKey is NewArrayExpression newArrayExpression
// Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630
// So there is no handling for dependent having TPT
table = targetEntityType.GetViewOrTableMappings().Single().Table;
var innerSelectExpression = _sqlExpressionFactory.Select(targetEntityType);

var innerSelectExpression = BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
entityProjectionExpression,
navigation,
foreignKey,
targetEntityType);

var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression);

var makeNullable = foreignKey.PrincipalKey.Properties
Expand All @@ -1212,14 +1238,60 @@ outerKey is NewArrayExpression newArrayExpression
innerShaper = new RelationalEntityShaperExpression(
targetEntityType,
_selectExpression.GenerateWeakEntityProjectionExpression(
targetEntityType, table, null, leftJoinTable, nullable: true)!,
targetEntityType,
table,
null,
leftJoinTable,
_sharedTypeEntityExpansionHelper,
nullable: true)!,
nullable: true);
}

entityProjectionExpression.AddNavigationBinding(navigation, innerShaper);
}

return innerShaper;

SelectExpression BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
EntityProjectionExpression entityProjectionExpression,
INavigation navigation,
IForeignKey foreignKey,
IEntityType targetEntityType)
{
// just need any column - we use it only to extract the table it originated from
var sourceColumn = entityProjectionExpression
.BindProperty(
navigation.IsOnDependent
? foreignKey.Properties[0]
: foreignKey.PrincipalKey.Properties[0]);

var sourceTable = FindRootTableExpressionForColumn(sourceColumn.Table, sourceColumn.Name);

return _sharedTypeEntityExpansionHelper.CreateInnerSelectExpression(
sourceTable,
targetEntityType);
}

static TableExpressionBase FindRootTableExpressionForColumn(TableExpressionBase table, string columnName)
{
if (table is JoinExpressionBase joinExpressionBase)
{
table = joinExpressionBase.Table;
}
else if (table is SetOperationBase setOperationBase)
{
table = setOperationBase.Source1;
}

if (table is SelectExpression selectExpression)
{
var matchingProjection = (ColumnExpression)selectExpression.Projection.Where(p => p.Alias == columnName).Single().Expression;

return FindRootTableExpressionForColumn(matchingProjection.Table, matchingProjection.Name);
}

return table;
}
}

private static Expression AddConvertToObject(Expression expression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ public sealed record RelationalQueryableMethodTranslatingExpressionVisitorDepend
[EntityFrameworkInternal]
public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
IRelationalSqlTranslatingExpressionVisitorFactory relationalSqlTranslatingExpressionVisitorFactory,
ISqlExpressionFactory sqlExpressionFactory)
ISqlExpressionFactory sqlExpressionFactory,
IRelationalSharedTypeEntityExpansionHelper relationalSharedTypeEntityExpansionHelper)
{
RelationalSqlTranslatingExpressionVisitorFactory = relationalSqlTranslatingExpressionVisitorFactory;
SqlExpressionFactory = sqlExpressionFactory;
RelationalSharedTypeEntityExpansionHelper = relationalSharedTypeEntityExpansionHelper;
}

/// <summary>
Expand All @@ -65,5 +67,10 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
/// The SQL expression factory.
/// </summary>
public ISqlExpressionFactory SqlExpressionFactory { get; init; }

/// <summary>
/// Shared type entity expansion helper.
/// </summary>
public IRelationalSharedTypeEntityExpansionHelper RelationalSharedTypeEntityExpansionHelper { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Query
{
/// <inheritdoc/>
public class RelationalSharedTypeEntityExpansionHelper : IRelationalSharedTypeEntityExpansionHelper
{
/// <summary>
/// Creates a new instance of the <see cref="RelationalSharedTypeEntityExpansionHelper" /> class.
/// </summary>
/// <param name="dependencies">Dependencies for this service.</param>
public RelationalSharedTypeEntityExpansionHelper(RelationalSharedTypeEntityExpansionHelperDependencies dependencies)
{
Dependencies = dependencies;
}

/// <summary>
/// Dependencies for this service.
/// </summary>
protected virtual RelationalSharedTypeEntityExpansionHelperDependencies Dependencies { get; }

/// <inheritdoc/>
public virtual SelectExpression CreateInnerSelectExpression(
TableExpressionBase sourceTable,
IEntityType targetEntityType)
=> Dependencies.SqlExpressionFactory.Select(targetEntityType);

/// <inheritdoc/>
public virtual bool TableMatchesMetadata(TableExpressionBase tableExpression, ITableBase tableMetadata)
=> tableExpression is TableExpression table
&& table.Name == tableMetadata.Name
&& table.Schema == tableMetadata.Schema;
}
}
Loading

0 comments on commit f919bec

Please sign in to comment.