Skip to content

Commit

Permalink
Validate more SqlServer-specific configuration on shared database obj…
Browse files Browse the repository at this point in the history
…ects

Refactor RelationalModelValidator

Part of #15660
  • Loading branch information
AndriySvyryd committed Mar 6, 2020
1 parent d3a2401 commit 9655da8
Show file tree
Hide file tree
Showing 15 changed files with 709 additions and 276 deletions.
23 changes: 21 additions & 2 deletions src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ namespace Microsoft.EntityFrameworkCore
public static class RelationalEntityTypeExtensions
{
/// <summary>
/// Returns the name of the table to which the entity type is mapped.
/// Returns the name of the table to which the entity type is mapped
/// or <c>null</c> if not mapped to a table.
/// </summary>
/// <param name="entityType"> The entity type to get the table name for. </param>
/// <returns> The name of the table to which the entity type is mapped. </returns>
Expand Down Expand Up @@ -161,6 +162,24 @@ public static void SetSchema(
=> entityType.FindAnnotation(RelationalAnnotationNames.Schema)
?.GetConfigurationSource();

/// <summary>
/// Returns the name of the table to which the entity type is mapped prepended by the schema
/// or <c>null</c> if not mapped to a table.
/// </summary>
/// <param name="entityType"> The entity type to get the table name for. </param>
/// <returns> The name of the table to which the entity type is mapped prepended by the schema. </returns>
public static string GetSchemaQualifiedTableName([NotNull] this IEntityType entityType)
{
var tableName = entityType.GetTableName();
if (tableName == null)
{
return null;
}

var schema = entityType.GetSchema();
return (string.IsNullOrEmpty(schema) ? "" : schema + ".") + tableName;
}

/// <summary>
/// Returns the tables to which the entity type is mapped.
/// </summary>
Expand Down Expand Up @@ -190,7 +209,7 @@ public static IEnumerable<IViewMapping> GetViewMappings([NotNull] this IEntityTy
?? Enumerable.Empty<IViewMapping>();

/// <summary>
/// Returns the name of the view to which the entity type is mapped.
/// Returns the name of the view to which the entity type is mapped or <c>null</c> if not mapped to a view.
/// </summary>
/// <param name="entityType"> The entity type to get the view name for. </param>
/// <returns> The name of the view to which the entity type is mapped. </returns>
Expand Down
319 changes: 189 additions & 130 deletions src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure
public static class RelationalPropertyExtensions
{
/// <summary>
/// Creates a comma-separated list of property names.
/// Creates a comma-separated list of column names.
/// </summary>
/// <param name="properties"> The properties to format. </param>
/// <returns> A comma-separated list of property names. </returns>
/// <returns> A comma-separated list of column names. </returns>
public static string FormatColumns([NotNull] this IEnumerable<IProperty> properties)
=> "{" + string.Join(", ", properties.Select(p => "'" + p.GetColumnName() + "'")) + "}";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ public static bool AreCompatible([NotNull] this IForeignKey foreignKey, [NotNull
foreignKey.DeclaringEntityType.DisplayName(),
duplicateForeignKey.Properties.Format(),
duplicateForeignKey.DeclaringEntityType.DisplayName(),
Format(foreignKey.DeclaringEntityType),
foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(),
foreignKey.GetConstraintName(),
Format(principalType),
Format(duplicatePrincipalType)));
principalType.GetSchemaQualifiedTableName(),
duplicatePrincipalType.GetSchemaQualifiedTableName()));
}

return false;
Expand All @@ -58,7 +58,7 @@ public static bool AreCompatible([NotNull] this IForeignKey foreignKey, [NotNull
foreignKey.DeclaringEntityType.DisplayName(),
duplicateForeignKey.Properties.Format(),
duplicateForeignKey.DeclaringEntityType.DisplayName(),
Format(foreignKey.DeclaringEntityType),
foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(),
foreignKey.GetConstraintName(),
foreignKey.Properties.FormatColumns(),
duplicateForeignKey.Properties.FormatColumns()));
Expand All @@ -81,7 +81,7 @@ public static bool AreCompatible([NotNull] this IForeignKey foreignKey, [NotNull
foreignKey.DeclaringEntityType.DisplayName(),
duplicateForeignKey.Properties.Format(),
duplicateForeignKey.DeclaringEntityType.DisplayName(),
Format(foreignKey.DeclaringEntityType),
foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(),
foreignKey.GetConstraintName(),
foreignKey.PrincipalKey.Properties.FormatColumns(),
duplicateForeignKey.PrincipalKey.Properties.FormatColumns()));
Expand All @@ -100,7 +100,7 @@ public static bool AreCompatible([NotNull] this IForeignKey foreignKey, [NotNull
foreignKey.DeclaringEntityType.DisplayName(),
duplicateForeignKey.Properties.Format(),
duplicateForeignKey.DeclaringEntityType.DisplayName(),
Format(foreignKey.DeclaringEntityType),
foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(),
foreignKey.GetConstraintName()));
}

Expand All @@ -117,7 +117,7 @@ public static bool AreCompatible([NotNull] this IForeignKey foreignKey, [NotNull
foreignKey.DeclaringEntityType.DisplayName(),
duplicateForeignKey.Properties.Format(),
duplicateForeignKey.DeclaringEntityType.DisplayName(),
Format(foreignKey.DeclaringEntityType),
foreignKey.DeclaringEntityType.GetSchemaQualifiedTableName(),
foreignKey.GetConstraintName(),
foreignKey.DeleteBehavior,
duplicateForeignKey.DeleteBehavior));
Expand All @@ -128,8 +128,5 @@ public static bool AreCompatible([NotNull] this IForeignKey foreignKey, [NotNull

return true;
}

private static string Format(IEntityType entityType)
=> (string.IsNullOrEmpty(entityType.GetSchema()) ? "" : entityType.GetSchema() + ".") + entityType.GetTableName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public static bool AreCompatible([NotNull] this IIndex index, [NotNull] IIndex d
index.DeclaringEntityType.DisplayName(),
duplicateIndex.Properties.Format(),
duplicateIndex.DeclaringEntityType.DisplayName(),
Format(index.DeclaringEntityType),
index.DeclaringEntityType.GetSchemaQualifiedTableName(),
index.GetName(),
index.Properties.FormatColumns(),
duplicateIndex.Properties.FormatColumns()));
Expand All @@ -55,7 +55,7 @@ public static bool AreCompatible([NotNull] this IIndex index, [NotNull] IIndex d
index.DeclaringEntityType.DisplayName(),
duplicateIndex.Properties.Format(),
duplicateIndex.DeclaringEntityType.DisplayName(),
Format(index.DeclaringEntityType),
index.DeclaringEntityType.GetSchemaQualifiedTableName(),
index.GetName()));
}

Expand All @@ -64,8 +64,5 @@ public static bool AreCompatible([NotNull] this IIndex index, [NotNull] IIndex d

return true;
}

private static string Format(IEntityType entityType)
=> (string.IsNullOrEmpty(entityType.GetSchema()) ? "" : entityType.GetSchema() + ".") + entityType.GetTableName();
}
}
2 changes: 1 addition & 1 deletion src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ private static void PopulateForeignKeyConstraints(Table table)
}

if (columns == null
|| StructuralComparisons.StructuralEqualityComparer.Equals(columns, principalColumns))
|| columns.SequenceEqual(principalColumns))
{
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -81,7 +80,7 @@ public bool Equals(ITableMapping x, ITableMapping y)
=> x.EntityType == y.EntityType
&& x.Table == y.Table
&& x.IncludesDerivedTypes == y.IncludesDerivedTypes
&& StructuralComparisons.StructuralEqualityComparer.Equals(x.ColumnMappings, y.ColumnMappings);
&& x.ColumnMappings.SequenceEqual(y.ColumnMappings);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -81,7 +80,7 @@ public bool Equals(IViewMapping x, IViewMapping y)
=> x.EntityType == y.EntityType
&& x.View == y.View
&& x.IncludesDerivedTypes == y.IncludesDerivedTypes
&& StructuralComparisons.StructuralEqualityComparer.Equals(x.ColumnMappings, y.ColumnMappings);
&& x.ColumnMappings.SequenceEqual(y.ColumnMappings);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,7 @@ public static bool CanSetIncludeProperties(

return (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention)
.Overrides(indexBuilder.Metadata.GetIncludePropertiesConfigurationSource())
|| StructuralComparisons.StructuralEqualityComparer.Equals(
propertyNames, indexBuilder.Metadata.GetIncludeProperties());
|| propertyNames.SequenceEqual(indexBuilder.Metadata.GetIncludeProperties());
}

/// <summary>
Expand Down
143 changes: 89 additions & 54 deletions src/EFCore.SqlServer/Internal/SqlServerModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -214,84 +215,118 @@ protected override void ValidateSharedColumnsCompatibility(
{
base.ValidateSharedColumnsCompatibility(mappedTypes, tableName, logger);

var identityColumns = new List<IProperty>();
var propertyMappings = new Dictionary<string, IProperty>();
var identityColumns = new Dictionary<string, IProperty>();

foreach (var property in mappedTypes.SelectMany(et => et.GetDeclaredProperties()))
{
var columnName = property.GetColumnName();
if (propertyMappings.TryGetValue(columnName, out var duplicateProperty))
if (property.GetValueGenerationStrategy() == SqlServerValueGenerationStrategy.IdentityColumn)
{
var propertyStrategy = property.GetValueGenerationStrategy();
var duplicatePropertyStrategy = duplicateProperty.GetValueGenerationStrategy();
if (propertyStrategy != duplicatePropertyStrategy
&& (propertyStrategy == SqlServerValueGenerationStrategy.IdentityColumn
|| duplicatePropertyStrategy == SqlServerValueGenerationStrategy.IdentityColumn))
identityColumns[property.GetColumnName()] = property;
}
}

if (identityColumns.Count > 1)
{
var sb = new StringBuilder()
.AppendJoin(identityColumns.Values.Select(p => "'" + p.DeclaringEntityType.DisplayName() + "." + p.Name + "'"));
throw new InvalidOperationException(SqlServerStrings.MultipleIdentityColumns(sb, tableName));
}
}

/// <inheritdoc />
protected override void ValidateCompatible(IProperty property, IProperty duplicateProperty, string columnName, string tableName)
{
base.ValidateCompatible(property, duplicateProperty, columnName, tableName);

var propertyStrategy = property.GetValueGenerationStrategy();
var duplicatePropertyStrategy = duplicateProperty.GetValueGenerationStrategy();
if (propertyStrategy != duplicatePropertyStrategy)
{
throw new InvalidOperationException(
SqlServerStrings.DuplicateColumnNameValueGenerationStrategyMismatch(
duplicateProperty.DeclaringEntityType.DisplayName(),
duplicateProperty.Name,
property.DeclaringEntityType.DisplayName(),
property.Name,
columnName,
tableName));
}

switch (propertyStrategy)
{
case SqlServerValueGenerationStrategy.IdentityColumn:
var increment = property.GetIdentityIncrement();
var duplicateIncrement = duplicateProperty.GetIdentityIncrement();
if (increment != duplicateIncrement)
{
throw new InvalidOperationException(
SqlServerStrings.DuplicateColumnNameValueGenerationStrategyMismatch(
SqlServerStrings.DuplicateColumnIdentityIncrementMismatch(
duplicateProperty.DeclaringEntityType.DisplayName(),
duplicateProperty.Name,
property.DeclaringEntityType.DisplayName(),
property.Name,
columnName,
tableName));
}
}
else
{
propertyMappings[columnName] = property;
if (property.GetValueGenerationStrategy() == SqlServerValueGenerationStrategy.IdentityColumn)

var seed = property.GetIdentitySeed();
var duplicateSeed = duplicateProperty.GetIdentitySeed();
if (seed != duplicateSeed)
{
identityColumns.Add(property);
throw new InvalidOperationException(
SqlServerStrings.DuplicateColumnIdentitySeedMismatch(
duplicateProperty.DeclaringEntityType.DisplayName(),
duplicateProperty.Name,
property.DeclaringEntityType.DisplayName(),
property.Name,
columnName,
tableName));
}
}
}

if (identityColumns.Count > 1)
{
var sb = new StringBuilder()
.AppendJoin(identityColumns.Select(p => "'" + p.DeclaringEntityType.DisplayName() + "." + p.Name + "'"));
throw new InvalidOperationException(SqlServerStrings.MultipleIdentityColumns(sb, tableName));
break;
case SqlServerValueGenerationStrategy.SequenceHiLo:
if (property.GetHiLoSequenceName() != duplicateProperty.GetHiLoSequenceName()
|| property.GetHiLoSequenceSchema() != duplicateProperty.GetHiLoSequenceSchema())
{
throw new InvalidOperationException(
SqlServerStrings.DuplicateColumnSequenceMismatch(
duplicateProperty.DeclaringEntityType.DisplayName(),
duplicateProperty.Name,
property.DeclaringEntityType.DisplayName(),
property.Name,
columnName,
tableName));
}

break;
}
}

/// <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 void ValidateSharedKeysCompatibility(
IReadOnlyList<IEntityType> mappedTypes, string tableName, IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger)
/// <inheritdoc />
protected override void ValidateCompatible(IKey key, IKey duplicateKey, string keyName, string tableName)
{
base.ValidateSharedKeysCompatibility(mappedTypes, tableName, logger);
base.ValidateCompatible(key, duplicateKey, keyName, tableName);

var keyMappings = new Dictionary<string, IKey>();

foreach (var key in mappedTypes.SelectMany(et => et.GetDeclaredKeys()))
if (key.IsClustered()
!= duplicateKey.IsClustered())
{
var keyName = key.GetName();
throw new InvalidOperationException(
SqlServerStrings.DuplicateKeyMismatchedClustering(
key.Properties.Format(),
key.DeclaringEntityType.DisplayName(),
duplicateKey.Properties.Format(),
duplicateKey.DeclaringEntityType.DisplayName(),
tableName,
keyName));
}
}

if (!keyMappings.TryGetValue(keyName, out var duplicateKey))
{
keyMappings[keyName] = key;
continue;
}
/// <inheritdoc />
protected override void ValidateCompatible(IIndex index, IIndex duplicateIndex, string indexName, string tableName)
{
base.ValidateCompatible(index, duplicateIndex, indexName, tableName);

if (key.IsClustered()
!= duplicateKey.IsClustered())
{
throw new InvalidOperationException(
SqlServerStrings.DuplicateKeyMismatchedClustering(
key.Properties.Format(),
key.DeclaringEntityType.DisplayName(),
duplicateKey.Properties.Format(),
duplicateKey.DeclaringEntityType.DisplayName(),
tableName,
keyName));
}
}
index.AreCompatibleForSqlServer(duplicateIndex, shouldThrow: true);
}
}
}
Loading

0 comments on commit 9655da8

Please sign in to comment.