Skip to content

Commit

Permalink
Add migration support for renaming foreign keys
Browse files Browse the repository at this point in the history
Some database systems support renaming foreign key constraints without
rebuilding the table.
Add a RenameForeignKey operation to MigrationBuilder in order to support
this.
  • Loading branch information
Brar committed Mar 11, 2019
1 parent 3d7e6c6 commit bf2d695
Show file tree
Hide file tree
Showing 15 changed files with 297 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,50 @@ protected virtual void Generate([NotNull] RenameColumnOperation operation, [NotN
}
}

/// <summary>
/// Generates code for a <see cref="RenameForeignKeyOperation" />.
/// </summary>
/// <param name="operation"> The operation. </param>
/// <param name="builder"> The builder code is added to. </param>
protected virtual void Generate([NotNull] RenameForeignKeyOperation operation, [NotNull] IndentedStringBuilder builder)
{
Check.NotNull(operation, nameof(operation));
Check.NotNull(builder, nameof(builder));

builder.AppendLine(".RenameForeignKey(");

using (builder.Indent())
{
builder
.Append("name: ")
.Append(Code.Literal(operation.Name));

if (operation.Schema != null)
{
builder
.AppendLine(",")
.Append("schema: ")
.Append(Code.Literal(operation.Schema));
}

if (operation.Table != null)
{
builder
.AppendLine(",")
.Append("table: ")
.Append(Code.Literal(operation.Table));
}

builder
.AppendLine(",")
.Append("newName: ")
.Append(Code.Literal(operation.NewName))
.Append(")");

Annotations(operation.GetAnnotations(), builder);
}
}

/// <summary>
/// Generates code for a <see cref="RenameIndexOperation" />.
/// </summary>
Expand Down
22 changes: 18 additions & 4 deletions src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class MigrationsModelDiffer : IMigrationsModelDiffer

private static readonly Type[] _alterOperationTypes = { typeof(AddPrimaryKeyOperation), typeof(AddUniqueConstraintOperation), typeof(AlterSequenceOperation) };

private static readonly Type[] _renameOperationTypes = { typeof(RenameColumnOperation), typeof(RenameIndexOperation), typeof(RenamePrimaryKeyOperation), typeof(RenameSequenceOperation), typeof(RenameUniqueConstraintOperation) };
private static readonly Type[] _renameOperationTypes = { typeof(RenameColumnOperation), typeof(RenameForeignKeyOperation), typeof(RenameIndexOperation), typeof(RenamePrimaryKeyOperation), typeof(RenameSequenceOperation), typeof(RenameUniqueConstraintOperation) };

private static readonly Type[] _columnOperationTypes = { typeof(AddColumnOperation), typeof(AlterColumnOperation) };

Expand Down Expand Up @@ -1143,8 +1143,7 @@ protected virtual IEnumerable<MigrationOperation> Diff(
Diff,
Add,
Remove,
(s, t, c) => s.Relational().Name == t.Relational().Name
&& s.Properties.Select(p => p.Relational().ColumnName).SequenceEqual(
(s, t, c) => s.Properties.Select(p => p.Relational().ColumnName).SequenceEqual(
t.Properties.Select(p => c.FindSource(p)?.Relational().ColumnName))
&& c.FindSourceTable(s.PrincipalEntityType)
== c.FindSource(c.FindTargetTable(t.PrincipalEntityType))
Expand All @@ -1159,7 +1158,22 @@ protected virtual IEnumerable<MigrationOperation> Diff(
/// </summary>
protected virtual IEnumerable<MigrationOperation> Diff(
[NotNull] IForeignKey source, [NotNull] IForeignKey target, [NotNull] DiffContext diffContext)
=> Enumerable.Empty<MigrationOperation>();
{
var targetEntityTypeAnnotations = target.DeclaringEntityType.RootType().Relational();
var sourceName = source.Relational().Name;
var targetName = target.Relational().Name;

if (sourceName != targetName)
{
yield return new RenameForeignKeyOperation
{
Schema = targetEntityTypeAnnotations.Schema,
Table = targetEntityTypeAnnotations.TableName,
Name = sourceName,
NewName = targetName
};
}
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
Expand Down
29 changes: 29 additions & 0 deletions src/EFCore.Relational/Migrations/MigrationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,35 @@ public virtual OperationBuilder<RenameColumnOperation> RenameColumn(
return new OperationBuilder<RenameColumnOperation>(operation);
}

/// <summary>
/// Builds an <see cref="RenameForeignKeyOperation" /> to rename an existing primary key.
/// </summary>
/// <param name="name"> The name of the primary key to be renamed.</param>
/// <param name="newName"> The new name for the primary key. </param>
/// <param name="table"> The table that the primary key belongs to. </param>
/// <param name="schema"> The schema that contains the table, or <c>null</c> to use the default schema. </param>
/// <returns> A builder to allow annotations to be added to the operation. </returns>
public virtual OperationBuilder<RenameForeignKeyOperation> RenameForeignKey(
[NotNull] string name,
[NotNull] string newName,
[CanBeNull] string table = null,
[CanBeNull] string schema = null)
{
Check.NotEmpty(name, nameof(name));
Check.NotEmpty(newName, nameof(newName));

var operation = new RenameForeignKeyOperation
{
Schema = schema,
Table = table,
Name = name,
NewName = newName
};
Operations.Add(operation);

return new OperationBuilder<RenameForeignKeyOperation>(operation);
}

/// <summary>
/// Builds an <see cref="RenameIndexOperation" /> to rename an existing index.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public class MigrationsSqlGenerator : IMigrationsSqlGenerator
{ typeof(DropUniqueConstraintOperation), (g, o, m, b) => g.Generate((DropUniqueConstraintOperation)o, m, b) },
{ typeof(EnsureSchemaOperation), (g, o, m, b) => g.Generate((EnsureSchemaOperation)o, m, b) },
{ typeof(RenameColumnOperation), (g, o, m, b) => g.Generate((RenameColumnOperation)o, m, b) },
{ typeof(RenameForeignKeyOperation), (g, o, m, b) => g.Generate((RenameForeignKeyOperation)o, m, b) },
{ typeof(RenameIndexOperation), (g, o, m, b) => g.Generate((RenameIndexOperation)o, m, b) },
{ typeof(RenamePrimaryKeyOperation), (g, o, m, b) => g.Generate((RenamePrimaryKeyOperation)o, m, b) },
{ typeof(RenameSequenceOperation), (g, o, m, b) => g.Generate((RenameSequenceOperation)o, m, b) },
Expand Down Expand Up @@ -349,6 +350,27 @@ protected virtual void Generate(
{
}

/// <summary>
/// <para>
/// Can be overridden by database providers to build commands for the given <see cref="RenameForeignKeyOperation" />
/// by making calls on the given <see cref="MigrationCommandListBuilder" />.
/// </para>
/// <para>
/// Note that the default implementation of this method throws <see cref="NotImplementedException" />. Providers
/// must override if they are to support this kind of operation.
/// </para>
/// </summary>
/// <param name="operation"> The operation. </param>
/// <param name="model"> The target model which may be <c>null</c> if the operations exist without a model. </param>
/// <param name="builder"> The command builder to use to build the commands. </param>
protected virtual void Generate(
[NotNull] RenameForeignKeyOperation operation,
[CanBeNull] IModel model,
[NotNull] MigrationCommandListBuilder builder)
{
throw new NotImplementedException();
}

/// <summary>
/// <para>
/// Can be overridden by database providers to build commands for the given <see cref="RenameIndexOperation" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 JetBrains.Annotations;

namespace Microsoft.EntityFrameworkCore.Migrations.Operations
{
/// <summary>
/// A <see cref="MigrationOperation" /> for renaming an existing foreign key.
/// </summary>
public class RenameForeignKeyOperation : MigrationOperation
{
/// <summary>
/// The old name of the foreign key.
/// </summary>
public virtual string Name { get; [param: NotNull] set; }

/// <summary>
/// The new name for the foreign key.
/// </summary>
public virtual string NewName { get; [param: NotNull] set; }

/// <summary>
/// The schema that contains the table, or <c>null</c> if the default schema should be used.
/// </summary>
public virtual string Schema { get; [param: CanBeNull] set; }

/// <summary>
/// The name of the table that the foreign key belongs to.
/// </summary>
public virtual string Table { get; [param: CanBeNull] set; }
}
}
28 changes: 28 additions & 0 deletions src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,34 @@ protected override void Generate(
builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table));
}

/// <summary>
/// Builds commands for the given <see cref="RenameForeignKeyOperation" />
/// by making calls on the given <see cref="MigrationCommandListBuilder" />.
/// </summary>
/// <param name="operation"> The operation. </param>
/// <param name="model"> The target model which may be <c>null</c> if the operations exist without a model. </param>
/// <param name="builder"> The command builder to use to build the commands. </param>
protected override void Generate(
RenameForeignKeyOperation operation,
IModel model,
MigrationCommandListBuilder builder)
{
Check.NotNull(operation, nameof(operation));
Check.NotNull(builder, nameof(builder));

if (string.IsNullOrEmpty(operation.Schema))
{
throw new InvalidOperationException(SqlServerStrings.ForeignKeySchemaRequired);
}

Rename(
Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema),
operation.NewName,
"OBJECT",
builder);
builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table));
}

/// <summary>
/// Builds commands for the given <see cref="RenameIndexOperation" />
/// by making calls on the given <see cref="MigrationCommandListBuilder" />.
Expand Down
6 changes: 6 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs

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

3 changes: 3 additions & 0 deletions src/EFCore.SqlServer/Properties/SqlServerStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,7 @@
<data name="UniqueConstraintSchemaRequired" xml:space="preserve">
<value>SQL Server requires the schema name to be specified for rename unique constraint operations. Specify schema name in the call to MigrationBuilder.RenameUniqueConstraint.</value>
</data>
<data name="ForeignKeySchemaRequired" xml:space="preserve">
<value>SQL Server requires the schema name to be specified for rename foreign key operations. Specify schema name in the call to MigrationBuilder.RenameForeignKey.</value>
</data>
</root>
11 changes: 11 additions & 0 deletions src/EFCore.Sqlite.Core/Migrations/SqliteMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,17 @@ protected override void Generate(AlterColumnOperation operation, IModel model, M
=> throw new NotSupportedException(
SqliteStrings.InvalidMigrationOperation(operation.GetType().ShortDisplayName()));

/// <summary>
/// Throws <see cref="NotSupportedException" /> since this operation requires table rebuilds, which
/// are not yet supported.
/// </summary>
/// <param name="operation"> The operation. </param>
/// <param name="model"> The target model which may be <c>null</c> if the operations exist without a model. </param>
/// <param name="builder"> The command builder to use to build the commands. </param>
protected override void Generate(RenameForeignKeyOperation operation, IModel model, MigrationCommandListBuilder builder)
=> throw new NotSupportedException(
SqliteStrings.InvalidMigrationOperation(operation.GetType().ShortDisplayName()));

/// <summary>
/// Throws <see cref="NotSupportedException" /> since this operation requires table rebuilds, which
/// are not yet supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1870,6 +1870,52 @@ public void RenameColumnOperation_all_args()
});
}

[Fact]
public void RenameForeignKeyOperation_required_args()
{
Test(
new RenameForeignKeyOperation
{
Name = "FK_Post_Title_Titles_Name",
NewName = "FK_Post_PostTitle_Titles_Name"
},
"mb.RenameForeignKey(" + _eol +
" name: \"FK_Post_Title_Titles_Name\"," + _eol +
" newName: \"FK_Post_PostTitle_Titles_Name\");",
o =>
{
Assert.Equal("FK_Post_Title_Titles_Name", o.Name);
Assert.Equal("FK_Post_PostTitle_Titles_Name", o.NewName);
Assert.Null(o.Table);
Assert.Null(o.Schema);
});
}

[Fact]
public void RenameForeignKeyOperation_all_args()
{
Test(
new RenameForeignKeyOperation
{
Name = "FK_Post_Title_Titles_Name",
Schema = "dbo",
Table = "Post",
NewName = "FK_Post_PostTitle_Titles_Name"
},
"mb.RenameForeignKey(" + _eol +
" name: \"FK_Post_Title_Titles_Name\"," + _eol +
" schema: \"dbo\"," + _eol +
" table: \"Post\"," + _eol +
" newName: \"FK_Post_PostTitle_Titles_Name\");",
o =>
{
Assert.Equal("FK_Post_Title_Titles_Name", o.Name);
Assert.Equal("dbo", o.Schema);
Assert.Equal("Post", o.Table);
Assert.Equal("FK_Post_PostTitle_Titles_Name", o.NewName);
});
}

[Fact]
public void RenameIndexOperation_required_args()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,17 @@ public virtual void RenameTableOperation()
NewSchema = "dbo"
});

[Fact]
public virtual void RenameForeignKeyOperation()
=> Generate(
new RenameForeignKeyOperation
{
Table = "People",
Schema = "dbo",
Name = "FK_People_Name_Names_Name",
NewName = "FK_People_FullName_Names_Name"
});

[Fact]
public virtual void RenamePrimaryKeyOperation()
=> Generate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2744,21 +2744,13 @@ public void Rename_foreign_key()
}),
operations =>
{
Assert.Equal(2, operations.Count);
var dropOperation = Assert.IsType<DropForeignKeyOperation>(operations[0]);
Assert.Equal("dbo", dropOperation.Schema);
Assert.Equal("Nematode", dropOperation.Table);
Assert.Equal("FK_Nematode_Nematode_ParentId", dropOperation.Name);
Assert.Equal(1, operations.Count);
var addOperation = Assert.IsType<AddForeignKeyOperation>(operations[1]);
Assert.Equal("dbo", addOperation.Schema);
Assert.Equal("Nematode", addOperation.Table);
Assert.Equal("FK_Nematode_NematodeParent", addOperation.Name);
Assert.Equal(new[] { "ParentId" }, addOperation.Columns);
Assert.Equal("dbo", addOperation.PrincipalSchema);
Assert.Equal("Nematode", addOperation.PrincipalTable);
Assert.Equal(new[] { "Id" }, addOperation.PrincipalColumns);
var renameOperation = Assert.IsType<RenameForeignKeyOperation>(operations[0]);
Assert.Equal("dbo", renameOperation.Schema);
Assert.Equal("Nematode", renameOperation.Table);
Assert.Equal("FK_Nematode_Nematode_ParentId", renameOperation.Name);
Assert.Equal("FK_Nematode_NematodeParent", renameOperation.NewName);
});
}

Expand Down Expand Up @@ -4043,12 +4035,11 @@ public void Rename_column_with_foreign_key()
}),
operations =>
{
Assert.Equal(4, operations.Count);
Assert.Equal(3, operations.Count);
Assert.IsType<DropForeignKeyOperation>(operations[0]);
Assert.IsType<RenameColumnOperation>(operations[1]);
Assert.IsType<RenameIndexOperation>(operations[2]);
Assert.IsType<AddForeignKeyOperation>(operations[3]);
Assert.IsType<RenameColumnOperation>(operations[0]);
Assert.IsType<RenameIndexOperation>(operations[1]);
Assert.IsType<RenameForeignKeyOperation>(operations[2]);
});
}

Expand Down
Loading

0 comments on commit bf2d695

Please sign in to comment.