Skip to content

Commit

Permalink
Fix table splitting for derived types in update pipeline.
Browse files Browse the repository at this point in the history
Make the columns nullable for dependent types involved in table splitting.
When uniquifying column names use the declaring entity type name as prefix.

Fixes #8907
  • Loading branch information
AndriySvyryd committed Jun 29, 2017
1 parent 2c25dac commit a7a2291
Show file tree
Hide file tree
Showing 26 changed files with 916 additions and 165 deletions.
105 changes: 105 additions & 0 deletions src/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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 Microsoft.EntityFrameworkCore.TestModels.TransportationModel;
using Xunit;

namespace Microsoft.EntityFrameworkCore
{
public abstract class TableSplittingTestBase<TTestStore>
where TTestStore : TestStore
{
[Fact]
public void Can_query_shared()
{
using (var store = CreateTestStore(OnModelCreating))
{
using (var context = CreateContext(store, OnModelCreating))
{
Assert.Equal(4, context.Set<Operator>().ToList().Count);
}
}
}

[Fact(Skip = "#8973")]
public void Can_query_shared_derived()
{
using (var store = CreateTestStore(OnModelCreating))
{
using (var context = CreateContext(store, OnModelCreating))
{
Assert.Equal(1, context.Set<FuelTank>().ToList().Count);
}
}
}

[Fact]
public void Can_use_with_redundant_relationships()
{
Test_roundtrip(OnModelCreating);
}

[Fact]
public void Can_use_with_chained_relationships()
{
Test_roundtrip(modelBuilder =>
{
OnModelCreating(modelBuilder);
modelBuilder.Entity<FuelTank>(eb =>
{
eb.Ignore(e => e.Vehicle);
});
});
}

[Fact]
public void Can_use_with_fanned_relationships()
{
Test_roundtrip(modelBuilder =>
{
OnModelCreating(modelBuilder);
modelBuilder.Entity<FuelTank>(eb =>
{
eb.Ignore(e => e.Engine);
});
modelBuilder.Entity<CombustionEngine>(eb =>
{
eb.Ignore(e => e.FuelTank);
});
});
}

protected virtual void OnModelCreating(ModelBuilder modelBuilder)
{
TransportationContext.OnModelCreatingBase(modelBuilder);

modelBuilder.Entity<Vehicle>(eb =>
{
eb.HasDiscriminator<string>("Discriminator");
eb.Property<string>("Discriminator").HasColumnName("Discriminator");
eb.ToTable("Vehicles");
});

modelBuilder.Entity<Engine>().ToTable("Vehicles");
modelBuilder.Entity<Operator>().ToTable("Vehicles");
modelBuilder.Entity<FuelTank>().ToTable("Vehicles");
}

protected void Test_roundtrip(Action<ModelBuilder> onModelCreating)
{
using (var store = CreateTestStore(onModelCreating))
{
using (var context = CreateContext(store, onModelCreating))
{
context.AssertSeeded();
}
}
}

protected static readonly string DatabaseName = "TableSplittingTest";
public abstract TTestStore CreateTestStore(Action<ModelBuilder> onModelCreating);
public abstract TransportationContext CreateContext(TTestStore testStore, Action<ModelBuilder> onModelCreating);
}
}
73 changes: 44 additions & 29 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,42 +199,57 @@ protected virtual void ValidateSharedTableCompatibility(
}

var firstValidatedType = mappedTypes[0];
var validatedTypes = new List<IEntityType> { firstValidatedType };
var unvalidatedTypes = new Queue<IEntityType>(mappedTypes.Skip(1));
while (unvalidatedTypes.Count > 0)
var typesToValidate = new Queue<IEntityType>();
typesToValidate.Enqueue(firstValidatedType);
var unvalidatedTypes = new HashSet<IEntityType>(mappedTypes.Skip(1));
while (typesToValidate.Count > 0)
{
var entityType = unvalidatedTypes.Dequeue();
var key = entityType.FindPrimaryKey();
var otherKey = firstValidatedType.FindPrimaryKey();
if (key.Relational().Name != otherKey.Relational().Name)
{
throw new InvalidOperationException(
RelationalStrings.IncompatibleTableKeyNameMismatch(
tableName,
entityType.DisplayName(),
firstValidatedType.DisplayName(),
key.Relational().Name,
Property.Format(key.Properties),
otherKey.Relational().Name,
Property.Format(otherKey.Properties)));
}

var relationshipFound = validatedTypes.Any(validatedType =>
var entityType = typesToValidate.Dequeue();
var typesToValidateLeft = typesToValidate.Count;
var nextTypeSet = unvalidatedTypes.Where(validatedType =>
entityType.RootType() == validatedType.RootType()
|| IsIdentifyingPrincipal(entityType, validatedType)
|| IsIdentifyingPrincipal(validatedType, entityType));
if (!relationshipFound)
foreach (var nextEntityType in nextTypeSet)
{
throw new InvalidOperationException(
RelationalStrings.IncompatibleTableNoRelationship(
tableName,
entityType.DisplayName(),
firstValidatedType.DisplayName(),
Property.Format(key.Properties),
Property.Format(otherKey.Properties)));
var key = entityType.FindPrimaryKey();
var otherKey = nextEntityType.FindPrimaryKey();
if (key.Relational().Name != otherKey.Relational().Name)
{
throw new InvalidOperationException(
RelationalStrings.IncompatibleTableKeyNameMismatch(
tableName,
entityType.DisplayName(),
nextEntityType.DisplayName(),
key.Relational().Name,
Property.Format(key.Properties),
otherKey.Relational().Name,
Property.Format(otherKey.Properties)));
}

typesToValidate.Enqueue(nextEntityType);
}

foreach (var typeToValidate in typesToValidate.Skip(typesToValidateLeft))
{
unvalidatedTypes.Remove(typeToValidate);
}
}

validatedTypes.Add(entityType);
if (unvalidatedTypes.Count == 0)
{
return;
}

foreach (var invalidEntityType in unvalidatedTypes)
{
throw new InvalidOperationException(
RelationalStrings.IncompatibleTableNoRelationship(
tableName,
invalidEntityType.DisplayName(),
firstValidatedType.DisplayName(),
Property.Format(invalidEntityType.FindPrimaryKey().Properties),
Property.Format(firstValidatedType.FindPrimaryKey().Properties)));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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.Infrastructure;
Expand Down Expand Up @@ -118,31 +119,47 @@ private static void TryUniquifyColumnNames(EntityType entityType, Dictionary<str
foreach (var property in entityType.GetDeclaredProperties())
{
var columnName = property.Relational().ColumnName;
if (properties.TryGetValue(columnName, out var otherProperty)
&& !property.IsPrimaryKey())
if (!properties.TryGetValue(columnName, out var otherProperty))
{
properties[columnName] = property;
continue;
}

if (!property.IsPrimaryKey())
{
var relationalPropertyBuilder = property.Builder.Relational(ConfigurationSource.Convention);
if (relationalPropertyBuilder.CanSetColumnName(null))
{
relationalPropertyBuilder.ColumnName = Uniquify(columnName, properties);
columnName = Uniquify(columnName, property.DeclaringEntityType.ShortName(), properties);
relationalPropertyBuilder.ColumnName = columnName;
properties[columnName] = property;
continue;
}
}

if (!otherProperty.IsPrimaryKey())
{
var otherRelationalPropertyBuilder = otherProperty.Builder.Relational(ConfigurationSource.Convention);
if (!otherRelationalPropertyBuilder.CanSetColumnName(null))
if (otherRelationalPropertyBuilder.CanSetColumnName(null))
{
continue;
properties[columnName] = property;
columnName = Uniquify(columnName, otherProperty.DeclaringEntityType.ShortName(), properties);
otherRelationalPropertyBuilder.ColumnName = columnName;
properties[columnName] = otherProperty;
}
otherRelationalPropertyBuilder.ColumnName = Uniquify(columnName, properties);
}
properties[columnName] = property;
}
}

private static string Uniquify<T>(string baseIdentifier, Dictionary<string, T> existingIdentifiers)
private static string Uniquify<T>(string baseIdentifier, string prefix, Dictionary<string, T> existingIdentifiers)
{
var finalIdentifier = baseIdentifier;
if (!baseIdentifier.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
baseIdentifier = prefix + "_" + baseIdentifier;
}

var suffix = 1;
var finalIdentifier = baseIdentifier;
while (existingIdentifiers.ContainsKey(finalIdentifier))
{
finalIdentifier = baseIdentifier + suffix;
Expand Down
21 changes: 12 additions & 9 deletions src/EFCore.Relational/Metadata/RelationalPropertyAnnotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,22 @@ public virtual string ColumnName

private string GetDefaultColumnName()
{
var entityType = Property.DeclaringEntityType;
var pk = Property.GetContainingPrimaryKey();
if (pk != null)
{
var entityType = Property.DeclaringEntityType;
var ownership = entityType.GetForeignKeys().SingleOrDefault(fk => fk.IsOwnership);
if (ownership != null)
foreach (var fk in entityType.FindForeignKeys(pk.Properties))
{
var ownerType = ownership.PrincipalEntityType;
if (!fk.PrincipalKey.IsPrimaryKey())
{
continue;
}

var principalEntityType = fk.PrincipalEntityType;
var entityTypeAnnotations = GetAnnotations(entityType);
var ownerTypeAnnotations = GetAnnotations(ownerType);
if (entityTypeAnnotations.TableName == ownerTypeAnnotations.TableName
&& entityTypeAnnotations.Schema == ownerTypeAnnotations.Schema)
var principalTypeAnnotations = GetAnnotations(principalEntityType);
if (entityTypeAnnotations.TableName == principalTypeAnnotations.TableName
&& entityTypeAnnotations.Schema == principalTypeAnnotations.Schema)
{
var index = -1;
for (var i = 0; i < pk.Properties.Count; i++)
Expand All @@ -71,13 +75,12 @@ private string GetDefaultColumnName()
}
}

return GetAnnotations(ownerType.FindPrimaryKey().Properties[index]).ColumnName;
return GetAnnotations(principalEntityType.FindPrimaryKey().Properties[index]).ColumnName;
}
}
}
else
{
var entityType = Property.DeclaringEntityType;
StringBuilder builder = null;
do
{
Expand Down
23 changes: 22 additions & 1 deletion src/EFCore.Relational/Metadata/RelationalPropertyExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,34 @@
// 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 JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.Metadata
{
public static class RelationalPropertyExtensions
{
public static bool IsColumnNullable([NotNull] this IProperty property)
=> property.DeclaringEntityType.BaseType != null || property.IsNullable;
{
if (property.DeclaringEntityType.BaseType != null
|| property.IsNullable)
{
return true;
}

if (property.IsPrimaryKey())
{
return false;
}

var pk = property.DeclaringEntityType.FindPrimaryKey();
return pk != null
&& property.DeclaringEntityType.FindForeignKeys(pk.Properties)
.Any(fk => fk.PrincipalKey.IsPrimaryKey()
&& fk.PrincipalEntityType.BaseType != null
&& fk.DeclaringEntityType.Relational().TableName == fk.PrincipalEntityType.Relational().TableName
&& fk.DeclaringEntityType.Relational().Schema == fk.PrincipalEntityType.Relational().Schema);
}
}
}
16 changes: 8 additions & 8 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

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

Loading

0 comments on commit a7a2291

Please sign in to comment.