Skip to content

Commit

Permalink
Temporal table support for owned and table splitting scenarios
Browse files Browse the repository at this point in the history
Fix to #26858 - Query: improve TableExpressionBase extensibility by adding annotations
Fix to #26469 - Query: enable temporal tables for table splitting scenarios
Fix to #26451 - Temporal Table: Owned Entities support?

Added annotation infra for TableExpressionBase and annotations for temporal table information. Removed (now unnecessary) temporal specific table expressions.
Also added temporal table support for owned typed and table splitting in general using the annotations to store the temporal information (no need for provider specific logic in places where we didn't have good extensibility)
For table splitting, every entity must explicitly define period start/end columns. They all need to be the same, but if not explicitly provided we try to uniquefy them. We should fix this so that you only need to specify it in one place but it's currently hard to do (hopefully convention layering will make it easier)

Fixes #26858
Fixes #26469
Fixes #26451
  • Loading branch information
maumar committed Jan 13, 2022
1 parent 84c52b3 commit 12cc146
Show file tree
Hide file tree
Showing 63 changed files with 7,781 additions and 632 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static EntityTypeBuilder ToTable(
{
Check.NotNull(buildAction, nameof(buildAction));

buildAction(new TableBuilder(entityTypeBuilder.Metadata));
buildAction(new TableBuilder(entityTypeBuilder));

return entityTypeBuilder;
}
Expand All @@ -76,7 +76,7 @@ public static EntityTypeBuilder ToTable(

entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(null);
buildAction(new TableBuilder(entityTypeBuilder.Metadata));
buildAction(new TableBuilder(entityTypeBuilder));

return entityTypeBuilder;
}
Expand Down Expand Up @@ -114,7 +114,7 @@ public static EntityTypeBuilder<TEntity> ToTable<TEntity>(
{
Check.NotNull(buildAction, nameof(buildAction));

buildAction(new TableBuilder<TEntity>(null, null, entityTypeBuilder.Metadata));
buildAction(new TableBuilder<TEntity>(null, null, entityTypeBuilder));

return entityTypeBuilder;
}
Expand All @@ -141,7 +141,7 @@ public static EntityTypeBuilder<TEntity> ToTable<TEntity>(

entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(null);
buildAction(new TableBuilder<TEntity>(name, null, entityTypeBuilder.Metadata));
buildAction(new TableBuilder<TEntity>(name, null, entityTypeBuilder));

return entityTypeBuilder;
}
Expand Down Expand Up @@ -192,7 +192,7 @@ public static EntityTypeBuilder ToTable(

entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(schema);
buildAction(new TableBuilder(entityTypeBuilder.Metadata));
buildAction(new TableBuilder(entityTypeBuilder));

return entityTypeBuilder;
}
Expand Down Expand Up @@ -240,7 +240,7 @@ public static EntityTypeBuilder<TEntity> ToTable<TEntity>(

entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(schema);
buildAction(new TableBuilder<TEntity>(name, schema, entityTypeBuilder.Metadata));
buildAction(new TableBuilder<TEntity>(name, schema, entityTypeBuilder));

return entityTypeBuilder;
}
Expand Down Expand Up @@ -277,11 +277,11 @@ public static OwnedNavigationBuilder ToTable(
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static OwnedNavigationBuilder ToTable(
this OwnedNavigationBuilder referenceOwnershipBuilder,
Action<TableBuilder> buildAction)
Action<OwnedNavigationTableBuilder> buildAction)
{
Check.NotNull(buildAction, nameof(buildAction));

buildAction(new TableBuilder(referenceOwnershipBuilder.OwnedEntityType));
buildAction(new OwnedNavigationTableBuilder(referenceOwnershipBuilder));

return referenceOwnershipBuilder;
}
Expand All @@ -297,13 +297,13 @@ public static OwnedNavigationBuilder ToTable(
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public static OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> ToTable<TOwnerEntity, TRelatedEntity>(
this OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> referenceOwnershipBuilder,
Action<TableBuilder<TRelatedEntity>> buildAction)
Action<OwnedNavigationTableBuilder<TRelatedEntity>> buildAction)
where TOwnerEntity : class
where TRelatedEntity : class
{
Check.NotNull(buildAction, nameof(buildAction));

buildAction(new TableBuilder<TRelatedEntity>(null, null, referenceOwnershipBuilder.OwnedEntityType));
buildAction(new OwnedNavigationTableBuilder<TRelatedEntity>(referenceOwnershipBuilder));

return referenceOwnershipBuilder;
}
Expand Down Expand Up @@ -337,14 +337,14 @@ public static OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> ToTable<TOwne
public static OwnedNavigationBuilder ToTable(
this OwnedNavigationBuilder referenceOwnershipBuilder,
string? name,
Action<TableBuilder> buildAction)
Action<OwnedNavigationTableBuilder> buildAction)
{
Check.NullButNotEmpty(name, nameof(name));
Check.NotNull(buildAction, nameof(buildAction));

referenceOwnershipBuilder.OwnedEntityType.SetTableName(name);
referenceOwnershipBuilder.OwnedEntityType.SetSchema(null);
buildAction(new TableBuilder(referenceOwnershipBuilder.OwnedEntityType));
buildAction(new OwnedNavigationTableBuilder(referenceOwnershipBuilder));

return referenceOwnershipBuilder;
}
Expand All @@ -362,7 +362,7 @@ public static OwnedNavigationBuilder ToTable(
public static OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> ToTable<TOwnerEntity, TRelatedEntity>(
this OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> referenceOwnershipBuilder,
string? name,
Action<TableBuilder<TRelatedEntity>> buildAction)
Action<OwnedNavigationTableBuilder<TRelatedEntity>> buildAction)
where TOwnerEntity : class
where TRelatedEntity : class
{
Expand All @@ -371,7 +371,7 @@ public static OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> ToTable<TOwne

referenceOwnershipBuilder.OwnedEntityType.SetTableName(name);
referenceOwnershipBuilder.OwnedEntityType.SetSchema(null);
buildAction(new TableBuilder<TRelatedEntity>(name, null, referenceOwnershipBuilder.OwnedEntityType));
buildAction(new OwnedNavigationTableBuilder<TRelatedEntity>(referenceOwnershipBuilder));

return referenceOwnershipBuilder;
}
Expand Down Expand Up @@ -415,15 +415,15 @@ public static OwnedNavigationBuilder ToTable(
this OwnedNavigationBuilder referenceOwnershipBuilder,
string name,
string? schema,
Action<TableBuilder> buildAction)
Action<OwnedNavigationTableBuilder> buildAction)
{
Check.NotNull(name, nameof(name));
Check.NullButNotEmpty(schema, nameof(schema));
Check.NotNull(buildAction, nameof(buildAction));

referenceOwnershipBuilder.OwnedEntityType.SetTableName(name);
referenceOwnershipBuilder.OwnedEntityType.SetSchema(schema);
buildAction(new TableBuilder(referenceOwnershipBuilder.OwnedEntityType));
buildAction(new OwnedNavigationTableBuilder(referenceOwnershipBuilder));

return referenceOwnershipBuilder;
}
Expand Down Expand Up @@ -462,7 +462,7 @@ public static OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> ToTable<TOwne
this OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> referenceOwnershipBuilder,
string name,
string? schema,
Action<TableBuilder<TRelatedEntity>> buildAction)
Action<OwnedNavigationTableBuilder<TRelatedEntity>> buildAction)
where TOwnerEntity : class
where TRelatedEntity : class
{
Expand All @@ -472,7 +472,7 @@ public static OwnedNavigationBuilder<TOwnerEntity, TRelatedEntity> ToTable<TOwne

referenceOwnershipBuilder.OwnedEntityType.SetTableName(name);
referenceOwnershipBuilder.OwnedEntityType.SetSchema(schema);
buildAction(new TableBuilder<TRelatedEntity>(name, schema, referenceOwnershipBuilder.OwnedEntityType));
buildAction(new OwnedNavigationTableBuilder<TRelatedEntity>(referenceOwnershipBuilder));

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

using System.ComponentModel;

namespace Microsoft.EntityFrameworkCore.Metadata.Builders;

/// <summary>
/// Instances of this class are returned from methods when using the <see cref="ModelBuilder" /> API
/// and it is not designed to be directly constructed in your application code.
/// </summary>
public class OwnedNavigationTableBuilder
{
/// <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>
[EntityFrameworkInternal]
public OwnedNavigationTableBuilder(OwnedNavigationBuilder ownedNavigationBuilder)
{
OwnedNavigationBuilder = ownedNavigationBuilder;
}

/// <summary>
/// The entity type being configured.
/// </summary>
public virtual IMutableEntityType Metadata => OwnedNavigationBuilder.OwnedEntityType;

/// <summary>
/// The entity type builder.
/// </summary>
public virtual OwnedNavigationBuilder OwnedNavigationBuilder { get; }

/// <summary>
/// Configures the table to be ignored by migrations.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information.
/// </remarks>
/// <param name="excluded">A value indicating whether the table should be managed by migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public virtual OwnedNavigationTableBuilder ExcludeFromMigrations(bool excluded = true)
{
Metadata.SetIsTableExcludedFromMigrations(excluded);

return this;
}

#region Hidden System.Object members

/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override string? ToString()
=> base.ToString();

/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns><see langword="true" /> if the specified object is equal to the current object; otherwise, <see langword="false" />.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj)
=> base.Equals(obj);

/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode()
=> base.GetHashCode();

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Metadata.Builders;

/// <summary>
/// Instances of this class are returned from methods when using the <see cref="ModelBuilder" /> API
/// and it is not designed to be directly constructed in your application code.
/// </summary>
/// <typeparam name="TEntity">The entity type being configured.</typeparam>
public class OwnedNavigationTableBuilder<TEntity> : OwnedNavigationTableBuilder
where TEntity : class
{
/// <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>
[EntityFrameworkInternal]
public OwnedNavigationTableBuilder(OwnedNavigationBuilder referenceOwnershipBuilder)
: base(referenceOwnershipBuilder)
{
}

/// <summary>
/// Configures the table to be ignored by migrations.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-migrations">Database migrations</see> for more information.
/// </remarks>
/// <param name="excluded">A value indicating whether the table should be managed by migrations.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public new virtual OwnedNavigationTableBuilder<TEntity> ExcludeFromMigrations(bool excluded = true)
=> (OwnedNavigationTableBuilder<TEntity>)base.ExcludeFromMigrations(excluded);
}
11 changes: 8 additions & 3 deletions src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ public class TableBuilder
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public TableBuilder(IMutableEntityType entityType)
public TableBuilder(EntityTypeBuilder entityTypeBuilder)
{
Metadata = entityType;
EntityTypeBuilder = entityTypeBuilder;
}

/// <summary>
/// The entity type being configured.
/// </summary>
public virtual IMutableEntityType Metadata { get; }
public virtual IMutableEntityType Metadata => EntityTypeBuilder.Metadata;

/// <summary>
/// The entity type builder.
/// </summary>
public virtual EntityTypeBuilder EntityTypeBuilder { get; }

/// <summary>
/// Configures the table to be ignored by migrations.
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public class TableBuilder<TEntity> : TableBuilder
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[EntityFrameworkInternal]
public TableBuilder(string? name, string? schema, IMutableEntityType entityType)
: base(entityType)
public TableBuilder(string? name, string? schema, EntityTypeBuilder entityTypeBuilder)
: base(entityTypeBuilder)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1080,11 +1080,16 @@ protected override Expression VisitExtension(Expression extensionExpression)
return null;
}

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

var innerShapedQuery = CreateShapedQueryExpression(
targetEntityType, innerSelectExpression);

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

var entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression);
var innerShaper = entityProjectionExpression.BindNavigation(navigation);
if (innerShaper == null)
{
Expand Down Expand Up @@ -1175,7 +1179,10 @@ 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);

var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression);

var makeNullable = foreignKey.PrincipalKey.Properties
Expand Down Expand Up @@ -1231,6 +1238,50 @@ outerKey is NewArrayExpression newArrayExpression
targetEntityType,
(ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression,
navigation);

SelectExpression BuildInnerSelectExpressionForOwnedTypeMappedToDifferentTable(
EntityProjectionExpression entityProjectionExpression,
INavigation navigation)
{
// 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);
var ownedTable = new TableExpression(targetEntityType.GetTableMappings().Single().Table);

foreach (var annotation in sourceTable.GetAnnotations())
{
ownedTable.SetAnnotation(annotation.Name, annotation.Value);
}

return _sqlExpressionFactory.Select(targetEntityType, ownedTable);
}

static TableExpressionBase FindRootTableExpressionForColumn(ColumnExpression column)
{
var table = column.Table;
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 == column.Name).Single().Expression;

return FindRootTableExpressionForColumn(matchingProjection);
}

return table;
}
}

private static Expression AddConvertToObject(Expression expression)
Expand Down
Loading

0 comments on commit 12cc146

Please sign in to comment.