Skip to content

Commit

Permalink
SqlServer Migrations: Add EXEC calls to idempotent script
Browse files Browse the repository at this point in the history
Also adds the ability for providers to detected whether a script is being generated and whether its idempotent. (resolves #14746)

Fixes #12911
  • Loading branch information
bricelam committed Aug 4, 2020
1 parent 40d99f0 commit 6540e7f
Show file tree
Hide file tree
Showing 15 changed files with 356 additions and 53 deletions.
4 changes: 2 additions & 2 deletions src/EFCore.Design/Design/Internal/MigrationsOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public virtual IEnumerable<MigrationInfo> GetMigrations(
public virtual string ScriptMigration(
[CanBeNull] string fromMigration,
[CanBeNull] string toMigration,
bool idempotent,
MigrationsSqlGenerationOptions options,
[CanBeNull] string contextType)
{
using var context = _contextOperations.CreateContext(contextType);
Expand All @@ -174,7 +174,7 @@ public virtual string ScriptMigration(

var migrator = services.GetRequiredService<IMigrator>();

return migrator.GenerateScript(fromMigration, toMigration, idempotent);
return migrator.GenerateScript(fromMigration, toMigration, options);
}

/// <summary>
Expand Down
12 changes: 10 additions & 2 deletions src/EFCore.Design/Design/OperationExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,19 @@ private string ScriptMigrationImpl(
[CanBeNull] string toMigration,
bool idempotent,
[CanBeNull] string contextType)
=> MigrationsOperations.ScriptMigration(
{
var options = MigrationsSqlGenerationOptions.Default;
if (idempotent)
{
options |= MigrationsSqlGenerationOptions.Idempotent;
}

return MigrationsOperations.ScriptMigration(
fromMigration,
toMigration,
idempotent,
options,
contextType);
}

/// <summary>
/// Represents an operation to remove the last migration.
Expand Down
4 changes: 3 additions & 1 deletion src/EFCore.Relational/Migrations/IMigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ public interface IMigrationsSqlGenerator
/// </summary>
/// <param name="operations"> The operations. </param>
/// <param name="model"> The target model which may be <see langword="null" /> if the operations exist without a model. </param>
/// <param name="options"> The options to use when generating commands. </param>
/// <returns> The list of commands to be executed or scripted. </returns>
IReadOnlyList<MigrationCommand> Generate(
[NotNull] IReadOnlyList<MigrationOperation> operations,
[CanBeNull] IModel model = null);
[CanBeNull] IModel model = null,
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default);
}
}
8 changes: 3 additions & 5 deletions src/EFCore.Relational/Migrations/IMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ Task MigrateAsync(
/// <param name="toMigration">
/// The target migration to migrate the database to, or <see langword="null" /> to migrate to the latest.
/// </param>
/// <param name="idempotent">
/// If <see langword="true" />, then idempotent scripts will be generated, otherwise
/// scripts will be generated that assume none of the migrations in the range specified have
/// already been applied to the database.
/// <param name="options">
/// The options to use when generating SQL for migrations.
/// </param>
/// <returns> The generated script. </returns>
string GenerateScript([CanBeNull] string fromMigration = null, [CanBeNull] string toMigration = null, bool idempotent = false);
string GenerateScript([CanBeNull] string fromMigration = null, [CanBeNull] string toMigration = null, MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default);
}
}
20 changes: 13 additions & 7 deletions src/EFCore.Relational/Migrations/Internal/Migrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,12 @@ protected virtual void PopulateMigrations(
public virtual string GenerateScript(
string fromMigration = null,
string toMigration = null,
bool idempotent = false)
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default)
{
options |= MigrationsSqlGenerationOptions.Script;

var idempotent = options.HasFlag(MigrationsSqlGenerationOptions.Idempotent);

IEnumerable<string> appliedMigrations;
if (string.IsNullOrEmpty(fromMigration)
|| fromMigration == Migration.InitialDatabase)
Expand Down Expand Up @@ -344,7 +348,7 @@ public virtual string GenerateScript(

_logger.MigrationGeneratingDownScript(this, migration, fromMigration, toMigration, idempotent);

foreach (var command in GenerateDownSql(migration, previousMigration))
foreach (var command in GenerateDownSql(migration, previousMigration, options))
{
if (idempotent)
{
Expand All @@ -369,7 +373,7 @@ public virtual string GenerateScript(
{
_logger.MigrationGeneratingUpScript(this, migration, fromMigration, toMigration, idempotent);

foreach (var command in GenerateUpSql(migration))
foreach (var command in GenerateUpSql(migration, options))
{
if (idempotent)
{
Expand Down Expand Up @@ -399,15 +403,16 @@ public virtual string GenerateScript(
/// 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 virtual IReadOnlyList<MigrationCommand> GenerateUpSql([NotNull] Migration migration)
protected virtual IReadOnlyList<MigrationCommand> GenerateUpSql([NotNull] Migration migration,
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default)
{
Check.NotNull(migration, nameof(migration));

var insertCommand = _rawSqlCommandBuilder.Build(
_historyRepository.GetInsertScript(new HistoryRow(migration.GetId(), ProductInfo.GetVersion())));

return _migrationsSqlGenerator
.Generate(migration.UpOperations, FinalizeModel(migration.TargetModel))
.Generate(migration.UpOperations, FinalizeModel(migration.TargetModel), options)
.Concat(new[] { new MigrationCommand(insertCommand, _currentContext.Context, _commandLogger) })
.ToList();
}
Expand All @@ -420,15 +425,16 @@ protected virtual IReadOnlyList<MigrationCommand> GenerateUpSql([NotNull] Migrat
/// </summary>
protected virtual IReadOnlyList<MigrationCommand> GenerateDownSql(
[NotNull] Migration migration,
[CanBeNull] Migration previousMigration)
[CanBeNull] Migration previousMigration,
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default)
{
Check.NotNull(migration, nameof(migration));

var deleteCommand = _rawSqlCommandBuilder.Build(
_historyRepository.GetDeleteScript(migration.GetId()));

return _migrationsSqlGenerator
.Generate(migration.DownOperations, FinalizeModel(previousMigration?.TargetModel))
.Generate(migration.DownOperations, FinalizeModel(previousMigration?.TargetModel), options)
.Concat(new[] { new MigrationCommand(deleteCommand, _currentContext.Context, _commandLogger) })
.ToList();
}
Expand Down
29 changes: 29 additions & 0 deletions src/EFCore.Relational/Migrations/MigrationsSqlGenerationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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;

namespace Microsoft.EntityFrameworkCore.Migrations
{
/// <summary>
/// The options to use when generating SQL for migrations.
/// </summary>
[Flags]
public enum MigrationsSqlGenerationOptions
{
/// <summary>
/// Generate SQL to execute at runtime.
/// </summary>
Default,

/// <summary>
/// Generate SQL for a script. Automatically added by <see cref="IMigrator.GenerateScript" />.
/// </summary>
Script,

/// <summary>
/// Generate SQL for an idempotent script.
/// </summary>
Idempotent
}
}
22 changes: 19 additions & 3 deletions src/EFCore.Relational/Migrations/MigrationsSqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,38 @@ protected virtual IUpdateSqlGenerator SqlGenerator
/// </summary>
protected virtual IComparer<string> VersionComparer { get; } = new SemanticVersionComparer();

/// <summary>
/// Gets or sets the options to use when generating commands.
/// </summary>
protected virtual MigrationsSqlGenerationOptions Options { get; set; }

/// <summary>
/// Generates commands from a list of operations.
/// </summary>
/// <param name="operations"> The operations. </param>
/// <param name="model"> The target model which may be <see langword="null" /> if the operations exist without a model. </param>
/// <param name="options"> The options to use when generating commands. </param>
/// <returns> The list of commands to be executed or scripted. </returns>
public virtual IReadOnlyList<MigrationCommand> Generate(
IReadOnlyList<MigrationOperation> operations,
IModel model = null)
IModel model = null,
MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default)
{
Check.NotNull(operations, nameof(operations));

Options = options;

var builder = new MigrationCommandListBuilder(Dependencies);
foreach (var operation in operations)
try
{
foreach (var operation in operations)
{
Generate(operation, model, builder);
}
}
finally
{
Generate(operation, model, builder);
Options = MigrationsSqlGenerationOptions.Default;
}

return builder.GetCommandList();
Expand Down
7 changes: 4 additions & 3 deletions src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,11 @@ public virtual Task CreateTablesAsync(CancellationToken cancellationToken = defa
/// <summary>
/// Gets the commands that will create all tables from the model.
/// </summary>
/// <param name="options"> The options to use when generating commands. </param>
/// <returns> The generated commands. </returns>
protected virtual IReadOnlyList<MigrationCommand> GetCreateTablesCommands()
protected virtual IReadOnlyList<MigrationCommand> GetCreateTablesCommands(MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default)
=> Dependencies.MigrationsSqlGenerator.Generate(
Dependencies.ModelDiffer.GetDifferences(null, Dependencies.Model.GetRelationalModel()), Dependencies.Model);
Dependencies.ModelDiffer.GetDifferences(null, Dependencies.Model.GetRelationalModel()), Dependencies.Model, options);

/// <summary>
/// Determines whether the database contains any tables. No attempt is made to determine if
Expand Down Expand Up @@ -291,7 +292,7 @@ public virtual async Task<bool> EnsureCreatedAsync(CancellationToken cancellatio
/// </returns>
public virtual string GenerateCreateScript()
{
var commands = GetCreateTablesCommands();
var commands = GetCreateTablesCommands(MigrationsSqlGenerationOptions.Script);
var builder = new StringBuilder();
foreach (var command in commands)
{
Expand Down
Loading

0 comments on commit 6540e7f

Please sign in to comment.