Detect incomplete migrations
This commit is contained in:
@@ -7,6 +7,9 @@ namespace Umbraco.Core.Migrations
|
||||
/// </summary>
|
||||
public interface IMigration : IDiscoverable
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the migration.
|
||||
/// </summary>
|
||||
void Migrate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user