Simplify migrations

This commit is contained in:
Stephan
2018-12-20 18:38:55 +01:00
parent b0eb0d1205
commit 4a71d14097
5 changed files with 200 additions and 57 deletions

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Migrations
{
/// <summary>
/// Represents a migration plan builder for merges.
/// </summary>
public class MergeBuilder
{
private readonly MigrationPlan _plan;
private readonly List<Type> _migrations = new List<Type>();
private string _withLast;
private bool _with;
/// <summary>
/// Initializes a new instance of the <see cref="MergeBuilder"/> class.
/// </summary>
internal MergeBuilder(MigrationPlan plan)
{
_plan = plan;
}
/// <summary>
/// Adds a transition to a target state through an empty migration.
/// </summary>
public MergeBuilder To(string targetState)
=> To<NoopMigration>(targetState);
/// <summary>
/// Adds a transition to a target state through a migration.
/// </summary>
public MergeBuilder To<TMigration>(string targetState)
where TMigration : IMigration
=> To(targetState, typeof(TMigration));
/// <summary>
/// Adds a transition to a target state through a migration.
/// </summary>
public MergeBuilder To(string targetState, Type migration)
{
if (_with)
{
_withLast = targetState;
targetState = _plan.CreateRandomState();
}
else
{
_migrations.Add(migration);
}
_plan.To(targetState, migration);
return this;
}
/// <summary>
/// Begins the second branch of the merge.
/// </summary>
public MergeBuilder With()
{
if (_with)
throw new InvalidOperationException("Cannot invoke With() twice.");
_with = true;
return this;
}
/// <summary>
/// Completes the merge.
/// </summary>
public MigrationPlan As(string targetState)
{
if (!_with)
throw new InvalidOperationException("Cannot invoke As() without invoking With() first.");
// reach final state
_plan.To(targetState);
// restart at former end of branch2
_plan.From(_withLast);
// and replay all branch1 migrations
foreach (var migration in _migrations)
_plan.To(_plan.CreateRandomState(), migration);
// reaching final state
_plan.To(targetState);
return _plan;
}
}
}

View File

@@ -72,8 +72,8 @@ namespace Umbraco.Core.Migrations
}
/// <summary>
/// Adds a transition to a target state through an empty migration.
/// </summary>
/// Adds a transition to a target state through an empty migration.
/// </summary>
public MigrationPlan To(string targetState)
=> To<NoopMigration>(targetState);
@@ -100,9 +100,39 @@ namespace Umbraco.Core.Migrations
}
/// <summary>
/// Adds transitions to a target state by copying transitions from a start state to an end state.
/// Adds a transition to a target state through a migration, replacing a previous migration.
/// </summary>
public MigrationPlan To(string targetState, string startState, string endState)
/// <typeparam name="TMigrationNew">The new migration.</typeparam>
/// <typeparam name="TMigrationRecover">The migration to use to recover from the previous target state.</typeparam>
/// <param name="recoverState">The previous target state, which we need to recover from through <typeparamref name="TMigrationRecover"/>.</param>
/// <param name="targetState">The new target state.</param>
public MigrationPlan ToWithReplace<TMigrationNew, TMigrationRecover>(string recoverState, string targetState)
where TMigrationNew: IMigration
where TMigrationRecover : IMigration
{
To<TMigrationNew>(targetState);
From(recoverState).To<TMigrationRecover>(targetState);
return this;
}
/// <summary>
/// Adds a transition to a target state through a migration, replacing a previous migration.
/// </summary>
/// <typeparam name="TMigrationNew">The new migration.</typeparam>
/// <param name="recoverState">The previous target state, which we can recover from directly.</param>
/// <param name="targetState">The new target state.</param>
public MigrationPlan ToWithReplace<TMigrationNew>(string recoverState, string targetState)
where TMigrationNew : IMigration
{
To<TMigrationNew>(targetState);
From(recoverState).To(targetState);
return this;
}
/// <summary>
/// Adds transitions to a target state by cloning transitions from a start state to an end state.
/// </summary>
public MigrationPlan ToWithClone(string startState, string endState, string targetState)
{
if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState));
if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState));
@@ -128,7 +158,7 @@ namespace Umbraco.Core.Migrations
var newTargetState = transition.TargetState == endState
? targetState
: Guid.NewGuid().ToString("B").ToUpper();
: CreateRandomState();
To(newTargetState, transition.MigrationType);
state = transition.TargetState;
}
@@ -136,6 +166,17 @@ namespace Umbraco.Core.Migrations
return this;
}
/// <summary>
/// Creates a random, unique state.
/// </summary>
public virtual string CreateRandomState()
=> Guid.NewGuid().ToString("B").ToUpper();
/// <summary>
/// Begins a merge.
/// </summary>
public MergeBuilder Merge() => new MergeBuilder(this);
/// <summary>
/// Gets the initial state.
/// </summary>
@@ -257,13 +298,14 @@ namespace Umbraco.Core.Migrations
/// Follows a path (for tests and debugging).
/// </summary>
/// <remarks>Does the same thing Execute does, but does not actually execute migrations.</remarks>
internal string FollowPath(string fromState, string toState = null)
internal IReadOnlyList<string> FollowPath(string fromState = null, string toState = null)
{
toState = toState.NullOrWhiteSpaceAsNull();
Validate();
var origState = fromState ?? string.Empty;
var states = new List<string> { origState };
if (!_transitions.TryGetValue(origState, out var transition))
throw new Exception($"Unknown state \"{origState}\".");
@@ -272,6 +314,7 @@ namespace Umbraco.Core.Migrations
{
var nextState = transition.TargetState;
origState = nextState;
states.Add(origState);
if (nextState == toState)
{
@@ -287,7 +330,7 @@ namespace Umbraco.Core.Migrations
if (origState != (toState ?? _finalState))
throw new Exception($"Internal error, reached state {origState} which is not state {toState ?? _finalState}");
return origState;
return states;
}
/// <summary>

View File

@@ -89,55 +89,27 @@ namespace Umbraco.Core.Migrations.Upgrade
To<SuperZero>("{9DF05B77-11D1-475C-A00A-B656AF7E0908}");
To<PropertyEditorsMigration>("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}");
To<LanguageColumns>("{7F59355A-0EC9-4438-8157-EB517E6D2727}");
//To<AddVariationTables1>("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); // AddVariationTables1 has been superseded by AddVariationTables2
To<AddVariationTables2>("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
// way out of the commented state
From("{941B2ABA-2D06-4E04-81F5-74224F1DB037}");
To<AddVariationTables1A>("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}");
// resume at {76DF5CD7-A884-41A5-8DC6-7860D95B1DF5} ...
ToWithReplace<AddVariationTables2, AddVariationTables1A>("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1
To<RefactorMacroColumns>("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}");
To<UserForeignKeys>("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path
//To<AddTypedLabels>("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove
To<AddTypedLabels>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but add it after shannon's, with a new target state
// way out of the commented state
From("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}");
To<UserForeignKeys>("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}");
// resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ...
Merge().To<UserForeignKeys>("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") // shannon added that one
.With().To<AddTypedLabels>("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") // stephan added that one
.As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}");
To<ContentVariationMigration>("{1350617A-4930-4D61-852F-E3AA9E692173}");
To<UpdateUmbracoConsent>("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0
//To<FallbackLanguage>("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); // andy added that one = merge conflict, remove
To<AddRelationTypeForMediaFolderOnDelete>("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}"); // from 7.12.0
To<IncreaseLanguageIsoCodeColumnLength>("{EB34B5DC-BB87-4005-985E-D983EA496C38}"); // from 7.12.0
To<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
To<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
//To<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
To<FallbackLanguage>("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state
// way out of andy's
From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}");
To("{8B14CEBD-EE47-4AAD-A841-93551D917F11}", "{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}");
// resume at {8B14CEBD-EE47-4AAD-A841-93551D917F11} ...
To<UpdateDefaultMandatoryLanguage>("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // add stephan's after others, with a new target state
// way out of the commented state
From("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}");
To<FallbackLanguage>("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
// resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ...
//To<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
To<RefactorVariantsModel>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
// way out of the commented state
From("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
To<FallbackLanguage>("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}");
// resume at {290C18EE-B3DE-4769-84F1-1F467F3F76DA}...
Merge()
.To<AddRelationTypeForMediaFolderOnDelete>("{0541A62B-EF87-4CA2-8225-F0EB98ECCC9F}") // from 7.12.0
.To<IncreaseLanguageIsoCodeColumnLength>("{EB34B5DC-BB87-4005-985E-D983EA496C38}") // from 7.12.0
.To<RenameTrueFalseField>("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}") // from 7.12.0
.To<SetDefaultTagsStorageType>("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}") // from 7.12.0
.With()
.To<FallbackLanguage>("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // andy added that one
.As("{8B14CEBD-EE47-4AAD-A841-93551D917F11}");
ToWithReplace<UpdateDefaultMandatoryLanguage>("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}", "{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // merge
ToWithReplace<RefactorVariantsModel>("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}", "{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); // merge
To<DropTaskTables>("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}");
To<FixLockTablePrimaryKey>("{77874C77-93E5-4488-A404-A630907CEEF0}");
To<AddLogTableColumns>("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
@@ -159,16 +131,22 @@ namespace Umbraco.Core.Migrations.Upgrade
From("{init-7.10.2}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.3}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.4}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.10.5}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.0}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.1}").To("{init-7.10.0}"); // same as 7.10.0
From("{init-7.11.2}").To("{init-7.10.0}"); // same as 7.10.0
// 7.12.0 has migrations, define a custom chain which copies the chain
// going from {init-7.10.0} to former final (1350617A) , and then goes straight to
// main chain, skipping the migrations
//
From("{init-7.12.0}");
// target copy from copy to (former final)
To("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}");
// start stop target
ToWithClone("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}");
From("{init-7.12.1}").To("{init-7.10.0}"); // same as 7.12.0
From("{init-7.12.2}").To("{init-7.10.0}"); // same as 7.12.0
From("{init-7.12.3}").To("{init-7.10.0}"); // same as 7.12.0
}
}
}

View File

@@ -339,6 +339,7 @@
<Compile Include="Manifest\ManifestContentAppFactory.cs" />
<Compile Include="Manifest\ManifestDashboardDefinition.cs" />
<Compile Include="Migrations\IncompleteMigrationExpressionException.cs" />
<Compile Include="Migrations\MergeBuilder.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" />

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moq;
using NPoco;
@@ -68,7 +69,6 @@ namespace Umbraco.Tests.Migrations
// save new state
kvs.SetValue("Umbraco.Tests.MigrationPlan", sourceState, state);
// fixme - what about post-migrations?
s.Complete();
}
@@ -140,11 +140,11 @@ namespace Umbraco.Tests.Migrations
var plan = new UmbracoPlan();
plan.Validate();
Console.WriteLine(plan.FinalState);
Assert.IsFalse(string.IsNullOrWhiteSpace(plan.FinalState));
Assert.IsFalse(plan.FinalState.IsNullOrWhiteSpace());
}
[Test]
public void CanCopyChain()
public void CanClone()
{
var plan = new MigrationPlan("default");
plan
@@ -157,14 +157,46 @@ namespace Umbraco.Tests.Migrations
plan
.From("xxx")
.To("yyy", "bbb", "ddd")
.ToWithClone("bbb", "ddd", "yyy")
.To("eee");
WritePlanToConsole(plan);
plan.Validate();
Assert.AreEqual("eee", plan.FollowPath("xxx"));
Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy"));
Assert.AreEqual("eee", plan.FollowPath("xxx").Last());
Assert.AreEqual("yyy", plan.FollowPath("xxx", "yyy").Last());
}
[Test]
public void CanMerge()
{
var plan = new MigrationPlan("default");
plan
.From(string.Empty)
.To("aaa")
.Merge()
.To("bbb")
.To("ccc")
.With()
.To("ddd")
.To("eee")
.As("fff")
.To("ggg");
WritePlanToConsole(plan);
plan.Validate();
AssertList(plan.FollowPath(), "", "aaa", "bbb", "ccc", "*", "*", "fff", "ggg");
AssertList(plan.FollowPath("ccc"), "ccc", "*", "*", "fff", "ggg");
AssertList(plan.FollowPath("eee"), "eee", "*", "*", "fff", "ggg");
}
private void AssertList(IReadOnlyList<string> states, params string[] expected)
{
Assert.AreEqual(expected.Length, states.Count, string.Join(", ", states));
for (var i = 0; i < expected.Length; i++)
if (expected[i] != "*")
Assert.AreEqual(expected[i], states[i], "at:" + i);
}
private void WritePlanToConsole(MigrationPlan plan)