Detect incomplete migrations

This commit is contained in:
Stephan
2018-11-15 19:34:32 +01:00
parent 0fedeab45d
commit 889e48ea4a
11 changed files with 172 additions and 12 deletions

View File

@@ -7,6 +7,9 @@ namespace Umbraco.Core.Migrations
/// </summary>
public interface IMigration : IDiscoverable
{
/// <summary>
/// Executes the migration.
/// </summary>
void Migrate();
}
}

View File

@@ -24,8 +24,13 @@ namespace Umbraco.Core.Migrations
ISqlContext SqlContext { get; }
/// <summary>
/// Gets the expression index.
/// Gets or sets the expression index.
/// </summary>
int Index { get; set; }
/// <summary>
/// Gets or sets a value indicating whether an expression is being built.
/// </summary>
bool BuildingExpression { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using System;
namespace Umbraco.Core.Migrations
{
/// <summary>
/// Represents errors that occurs when a migration exception is not executed.
/// </summary>
/// <remarks>
/// <para>Migration expression such as Alter.Table(...).Do() *must* end with Do() else they are
/// not executed. When a non-executed expression is detected, an IncompleteMigrationExpressionException
/// is thrown.</para>
/// </remarks>
public class IncompleteMigrationExpressionException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="IncompleteMigrationExpressionException"/> class.
/// </summary>
public IncompleteMigrationExpressionException()
{ }
/// <summary>
/// Initializes a new instance of the <see cref="IncompleteMigrationExpressionException"/> class with a message.
/// </summary>
public IncompleteMigrationExpressionException(string message)
: base(message)
{ }
}
}

View File

@@ -62,42 +62,65 @@ namespace Umbraco.Core.Migrations
/// </summary>
protected Sql<ISqlContext> Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args);
/// <inheritdoc />
/// <summary>
/// Executes the migration.
/// </summary>
public abstract void Migrate();
/// <inheritdoc />
void IMigration.Migrate()
{
Migrate();
// ensure there is no building expression
// ie we did not forget to .Do() an expression
if (Context.BuildingExpression)
throw new IncompleteMigrationExpressionException("The migration has run, but leaves an expression that has not run.");
}
// ensures we are not already building,
// ie we did not forget to .Do() an expression
private T BeginBuild<T>(T builder)
{
if (Context.BuildingExpression)
throw new IncompleteMigrationExpressionException("Cannot create a new expression: the previous expression has not run.");
Context.BuildingExpression = true;
return builder;
}
/// <summary>
/// Builds an Alter expression.
/// </summary>
public IAlterBuilder Alter => new AlterBuilder(Context);
public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context));
/// <summary>
/// Builds a Create expression.
/// </summary>
public ICreateBuilder Create => new CreateBuilder(Context);
public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context));
/// <summary>
/// Builds a Delete expression.
/// </summary>
public IDeleteBuilder Delete => new DeleteBuilder(Context);
public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context));
/// <summary>
/// Builds an Execute expression.
/// </summary>
public IExecuteBuilder Execute => new ExecuteBuilder(Context);
public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context));
/// <summary>
/// Builds an Insert expression.
/// </summary>
public IInsertBuilder Insert => new InsertBuilder(Context);
public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context));
/// <summary>
/// Builds a Rename expression.
/// </summary>
public IRenameBuilder Rename => new RenameBuilder(Context);
public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context));
/// <summary>
/// Builds an Update expression.
/// </summary>
public IUpdateBuilder Update => new UpdateBuilder(Context);
public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context));
}
}

View File

@@ -4,20 +4,33 @@ using Umbraco.Core.Persistence;
namespace Umbraco.Core.Migrations
{
/// <summary>
/// Represents a migration context.
/// </summary>
internal class MigrationContext : IMigrationContext
{
/// <summary>
/// Initializes a new instance of the <see cref="MigrationContext"/> class.
/// </summary>
public MigrationContext(IUmbracoDatabase database, ILogger logger)
{
Database = database ?? throw new ArgumentNullException(nameof(database));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public ILogger Logger { get; }
/// <inheritdoc />
public IUmbracoDatabase Database { get; }
/// <inheritdoc />
public ISqlContext SqlContext => Database.SqlContext;
/// <inheritdoc />
public int Index { get; set; }
/// <inheritdoc />
public bool BuildingExpression { get; set; }
}
}

View File

@@ -50,6 +50,7 @@ namespace Umbraco.Core.Migrations
if (_executed)
throw new InvalidOperationException("This expression has already been executed.");
_executed = true;
Context.BuildingExpression = false;
var sql = GetSql();

View File

@@ -119,7 +119,7 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
Chain<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
//Chain<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
Chain<FallbackLanguage>("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state
From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's
.CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next
@@ -141,6 +141,7 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain<AddLogTableColumns>("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
Chain<DropPreValueTable>("{23275462-446E-44C7-8C2C-3B8C1127B07D}");
Chain<DropDownPropertyEditorsMigration>("{6B251841-3069-4AD5-8AE9-861F9523E8DA}");
Chain<TagsMigrationFix>("{EE429F1B-9B26-43CA-89F8-A86017C809A3}");
//FINAL

View File

@@ -18,7 +18,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
// kill unused parentId column
Delete.ForeignKey("FK_cmsTags_cmsTags").OnTable(Constants.DatabaseSchema.Tables.Tag).Do();
Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag);
Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do();
}
}
// fixes TagsMigration that... originally failed to properly drop the ParentId column
}

View File

@@ -0,0 +1,16 @@
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
public class TagsMigrationFix : MigrationBase
{
public TagsMigrationFix(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
// kill unused parentId column, if it still exists
if (ColumnExists(Constants.DatabaseSchema.Tables.Tag, "ParentId"))
Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do();
}
}
}

View File

@@ -335,6 +335,7 @@
<Compile Include="Logging\Serilog\Enrichers\Log4NetLevelMapperEnricher.cs" />
<Compile Include="Manifest\ContentAppDefinitionConverter.cs" />
<Compile Include="Manifest\ManifestContentAppDefinition.cs" />
<Compile Include="Migrations\IncompleteMigrationExpressionException.cs" />
<Compile Include="Migrations\MigrationBase_Extra.cs" />
<Compile Include="Migrations\Upgrade\V_7_10_0\RenamePreviewFolder.cs" />
<Compile Include="Migrations\Upgrade\V_7_12_0\AddRelationTypeForMediaFolderOnDelete.cs" />
@@ -367,6 +368,7 @@
<Compile Include="Migrations\Upgrade\V_8_0_0\SuperZero.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\TagsMigration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\FallbackLanguage.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\TagsMigrationFix.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\UpdateDefaultMandatoryLanguage.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\UserForeignKeys.cs" />
<Compile Include="Models\AuditEntry.cs" />

View File

@@ -1,16 +1,19 @@
using System;
using System.Data;
using Moq;
using NUnit.Framework;
using Semver;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
using Umbraco.Core.Migrations;
using Umbraco.Core.Migrations.Upgrade;
using Umbraco.Core.Persistence;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using ILogger = Umbraco.Core.Logging.ILogger;
namespace Umbraco.Tests.Migrations
{
[TestFixture]
public class MigrationTests
{
public class TestUpgrader : Upgrader
@@ -67,5 +70,68 @@ namespace Umbraco.Tests.Migrations
public IScopeContext Context { get; set; }
public ISqlContext SqlContext { get; set; }
}
[Test]
public void RunGoodMigration()
{
var migrationContext = new MigrationContext(Mock.Of<IUmbracoDatabase>(), Mock.Of<ILogger>());
IMigration migration = new GoodMigration(migrationContext);
migration.Migrate();
}
[Test]
public void DetectBadMigration1()
{
var migrationContext = new MigrationContext(Mock.Of<IUmbracoDatabase>(), Mock.Of<ILogger>());
IMigration migration = new BadMigration1(migrationContext);
Assert.Throws<IncompleteMigrationExpressionException>(() => migration.Migrate());
}
[Test]
public void DetectBadMigration2()
{
var migrationContext = new MigrationContext(Mock.Of<IUmbracoDatabase>(), Mock.Of<ILogger>());
IMigration migration = new BadMigration2(migrationContext);
Assert.Throws<IncompleteMigrationExpressionException>(() => migration.Migrate());
}
public class GoodMigration : MigrationBase
{
public GoodMigration(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
Execute.Sql("").Do();
}
}
public class BadMigration1 : MigrationBase
{
public BadMigration1(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
Alter.Table("foo"); // stop here, don't Do it
}
}
public class BadMigration2 : MigrationBase
{
public BadMigration2(IMigrationContext context)
: base(context)
{ }
public override void Migrate()
{
Alter.Table("foo"); // stop here, don't Do it
// and try to start another one
Alter.Table("bar");
}
}
}
}