From d029204bab4551886bd96147a81f8d138cc25ba5 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 14:26:12 +0200 Subject: [PATCH 1/6] Fix deterministic migration planning and transaction cleanup --- src/Migrator.Tests/MigrationLoaderTest.cs | 1 + src/Migrator.Tests/MigratorTest.cs | 473 +++++++++--------- src/Migrator.Tests/RunnerSafetyTests.cs | 79 +++ src/Migrator/Framework/IMigrationHistory.cs | 10 + src/Migrator/MigrateAnywhere.cs | 75 +-- src/Migrator/MigrationExecution.cs | 57 +++ src/Migrator/MigrationLoader.cs | 345 ++++++------- src/Migrator/MigrationPlan.cs | 25 + src/Migrator/Migrator.cs | 66 ++- .../SQLite/SQLiteTransformationProvider.cs | 19 +- .../Providers/TransformationProvider.cs | 123 ++--- 11 files changed, 665 insertions(+), 608 deletions(-) create mode 100644 src/Migrator.Tests/RunnerSafetyTests.cs create mode 100644 src/Migrator/Framework/IMigrationHistory.cs create mode 100644 src/Migrator/MigrationExecution.cs create mode 100644 src/Migrator/MigrationPlan.cs diff --git a/src/Migrator.Tests/MigrationLoaderTest.cs b/src/Migrator.Tests/MigrationLoaderTest.cs index fec13989..7394d1f0 100644 --- a/src/Migrator.Tests/MigrationLoaderTest.cs +++ b/src/Migrator.Tests/MigrationLoaderTest.cs @@ -40,6 +40,7 @@ private void SetUpCurrentVersion(int version, bool assertRollbackIsCalled) }); _migrationLoader = new MigrationLoader(providerMock, Assembly.GetExecutingAssembly(), true); + _migrationLoader.MigrationsTypes.Clear(); _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.FirstMigration)); _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.SecondMigration)); _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.ThirdMigration)); diff --git a/src/Migrator.Tests/MigratorTest.cs b/src/Migrator.Tests/MigratorTest.cs index 881cd519..88f362e0 100644 --- a/src/Migrator.Tests/MigratorTest.cs +++ b/src/Migrator.Tests/MigratorTest.cs @@ -1,248 +1,225 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using DotNetProjects.Migrator; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Loggers; -using NSubstitute; -using NUnit.Framework; - -namespace Migrator.Tests; - -[TestFixture] -public class MigratorTest -{ - #region Setup/Teardown - - [SetUp] - public void SetUp() - { - SetUpCurrentVersion(0); - } - - #endregion - - private DotNetProjects.Migrator.Migrator _migrator; - - // Collections that contain the version that are called migrating up and down - private static readonly List _upCalled = new List(); - private static readonly List _downCalled = new List(); - - private void SetUpCurrentVersion(long version) - { - SetUpCurrentVersion(version, false); - } - - private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled) - { - SetUpCurrentVersion(version, assertRollbackIsCalled, true); - } - - private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled, bool includeBad) - { - var providerMock = Substitute.For(); - - var appliedVersions = new List(); - - for (long i = 1; i <= version; i++) - { - appliedVersions.Add(i); - } - - providerMock.AppliedMigrations.Returns(appliedVersions); - providerMock.Logger.Returns(new Logger(false)); - - providerMock.When(x => x.Dispose()).Do(_ => - { - if (assertRollbackIsCalled) - { - providerMock.Received().Rollback(); - } - else - { - providerMock.DidNotReceive().Rollback(); - } - }); - - _migrator = new DotNetProjects.Migrator.Migrator((ITransformationProvider)providerMock, Assembly.GetExecutingAssembly(), false); - - _migrator.MigrationsTypes.Clear(); - _upCalled.Clear(); - _downCalled.Clear(); - - _migrator.MigrationsTypes.Add(typeof(FirstMigration)); - _migrator.MigrationsTypes.Add(typeof(SecondMigration)); - _migrator.MigrationsTypes.Add(typeof(ThirdMigration)); - _migrator.MigrationsTypes.Add(typeof(ForthMigration)); - _migrator.MigrationsTypes.Add(typeof(SixthMigration)); - - if (includeBad) - { - _migrator.MigrationsTypes.Add(typeof(BadMigration)); - } - } - - public class AbstractTestMigration : Migration - { - public override void Up() - { - _upCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); - } - - public override void Down() - { - _downCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); - } - } - - [Migration(1, Ignore = true)] - public class FirstMigration : AbstractTestMigration - { - } - - [Migration(2, Ignore = true)] - public class SecondMigration : AbstractTestMigration - { - } - - [Migration(3, Ignore = true)] - public class ThirdMigration : AbstractTestMigration - { - } - - [Migration(4, Ignore = true)] - public class ForthMigration : AbstractTestMigration - { - } - - [Migration(5, Ignore = true)] - public class BadMigration : AbstractTestMigration - { - public override void Up() - { - throw new Exception("oh uh!"); - } - - public override void Down() - { - throw new Exception("oh uh!"); - } - } - - [Migration(6, Ignore = true)] - public class SixthMigration : AbstractTestMigration - { - } - - [Migration(7)] - public class NonIgnoredMigration : AbstractTestMigration - { - } - - [Test] - public void MigrateBackward() - { - SetUpCurrentVersion(3); - _migrator.MigrateTo(1); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(2, Is.EqualTo(_downCalled.Count)); - - Assert.That(3, Is.EqualTo(_downCalled[0])); - Assert.That(2, Is.EqualTo(_downCalled[1])); - } - - [Test] - public void MigrateDownwardWithRollback() - { - SetUpCurrentVersion(6, true); - - try - { - _migrator.MigrateTo(3); - Assert.Fail("La migration 5 devrait lancer une exception"); - } - catch (Exception) - { - } - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(1, Is.EqualTo(_downCalled.Count)); - - Assert.That(6, Is.EqualTo(_downCalled[0])); - } - - [Test] - public void MigrateToCurrentVersion() - { - SetUpCurrentVersion(3); - - _migrator.MigrateTo(3); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - } - - [Test] - public void MigrateToLastVersion() - { - SetUpCurrentVersion(3, false, false); - - _migrator.MigrateToLastVersion(); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - } - - [Test] - public void MigrateUpward() - { - SetUpCurrentVersion(1); - _migrator.MigrateTo(3); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2, Is.EqualTo(_upCalled[0])); - Assert.That(3, Is.EqualTo(_upCalled[1])); - } - - [Test] - public void MigrateUpwardFrom0() - { - _migrator.MigrateTo(3); - - Assert.That(3, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(1, Is.EqualTo(_upCalled[0])); - Assert.That(2, Is.EqualTo(_upCalled[1])); - Assert.That(3, Is.EqualTo(_upCalled[2])); - } - - [Test] - public void MigrateUpwardWithRollback() - { - SetUpCurrentVersion(3, true); - - try - { - _migrator.MigrateTo(6); - Assert.Fail("La migration 5 devrait lancer une exception"); - } - catch (Exception) - { - } - - Assert.That(1, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(4, Is.EqualTo(_upCalled[0])); - } - - [Test] - public void ToHumanName() - { - Assert.That("Create a table", Is.EqualTo(StringUtils.ToHumanName("CreateATable"))); - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using NSubstitute; +using NUnit.Framework; + +namespace Migrator.Tests; + +[TestFixture] +public class MigratorTest +{ + #region Setup/Teardown + + [SetUp] + public void SetUp() + { + SetUpCurrentVersion(0); + } + + #endregion + + private DotNetProjects.Migrator.Migrator _migrator; + private ITransformationProvider _provider; + + // Collections that contain the version that are called migrating up and down + private static readonly List _upCalled = new List(); + private static readonly List _downCalled = new List(); + + private void SetUpCurrentVersion(long version) + { + SetUpCurrentVersion(version, false); + } + + private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled) + { + SetUpCurrentVersion(version, assertRollbackIsCalled, true); + } + + private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled, bool includeBad) + { + var providerMock = _provider = Substitute.For(); + + var appliedVersions = new List(); + + for (long i = 1; i <= version; i++) + { + appliedVersions.Add(i); + } + + providerMock.AppliedMigrations.Returns(appliedVersions); + providerMock.Logger.Returns(new Logger(false)); + + _migrator = new DotNetProjects.Migrator.Migrator((ITransformationProvider)providerMock, Assembly.GetExecutingAssembly(), false); + + _migrator.MigrationsTypes.Clear(); + _upCalled.Clear(); + _downCalled.Clear(); + + _migrator.MigrationsTypes.Add(typeof(FirstMigration)); + _migrator.MigrationsTypes.Add(typeof(SecondMigration)); + _migrator.MigrationsTypes.Add(typeof(ThirdMigration)); + _migrator.MigrationsTypes.Add(typeof(ForthMigration)); + _migrator.MigrationsTypes.Add(typeof(SixthMigration)); + + if (includeBad) + { + _migrator.MigrationsTypes.Add(typeof(BadMigration)); + } + } + + public class AbstractTestMigration : Migration + { + public override void Up() + { + _upCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); + } + + public override void Down() + { + _downCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); + } + } + + [Migration(1, Ignore = true)] + public class FirstMigration : AbstractTestMigration + { + } + + [Migration(2, Ignore = true)] + public class SecondMigration : AbstractTestMigration + { + } + + [Migration(3, Ignore = true)] + public class ThirdMigration : AbstractTestMigration + { + } + + [Migration(4, Ignore = true)] + public class ForthMigration : AbstractTestMigration + { + } + + [Migration(5, Ignore = true)] + public class BadMigration : AbstractTestMigration + { + public override void Up() + { + throw new Exception("oh uh!"); + } + + public override void Down() + { + throw new Exception("oh uh!"); + } + } + + [Migration(6, Ignore = true)] + public class SixthMigration : AbstractTestMigration + { + } + + [Migration(7)] + public class NonIgnoredMigration : AbstractTestMigration + { + } + + [Test] + public void MigrateBackward() + { + SetUpCurrentVersion(3); + _migrator.MigrateTo(1); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(2, Is.EqualTo(_downCalled.Count)); + + Assert.That(3, Is.EqualTo(_downCalled[0])); + Assert.That(2, Is.EqualTo(_downCalled[1])); + } + + [Test] + public void MigrateDownwardWithRollback() + { + SetUpCurrentVersion(6, true); + + Assert.Throws(() => _migrator.MigrateTo(3)); + _provider.Received(1).Rollback(); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(1, Is.EqualTo(_downCalled.Count)); + + Assert.That(6, Is.EqualTo(_downCalled[0])); + } + + [Test] + public void MigrateToCurrentVersion() + { + SetUpCurrentVersion(3); + + _migrator.MigrateTo(3); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + } + + [Test] + public void MigrateToLastVersion() + { + SetUpCurrentVersion(3, false, false); + + _migrator.MigrateToLastVersion(); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + } + + [Test] + public void MigrateUpward() + { + SetUpCurrentVersion(1); + _migrator.MigrateTo(3); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2, Is.EqualTo(_upCalled[0])); + Assert.That(3, Is.EqualTo(_upCalled[1])); + } + + [Test] + public void MigrateUpwardFrom0() + { + _migrator.MigrateTo(3); + + Assert.That(3, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(1, Is.EqualTo(_upCalled[0])); + Assert.That(2, Is.EqualTo(_upCalled[1])); + Assert.That(3, Is.EqualTo(_upCalled[2])); + } + + [Test] + public void MigrateUpwardWithRollback() + { + SetUpCurrentVersion(3, true); + + Assert.Throws(() => _migrator.MigrateTo(6)); + _provider.Received(1).Rollback(); + + Assert.That(1, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(4, Is.EqualTo(_upCalled[0])); + } + + [Test] + public void ToHumanName() + { + Assert.That("Create a table", Is.EqualTo(StringUtils.ToHumanName("CreateATable"))); + } +} diff --git a/src/Migrator.Tests/RunnerSafetyTests.cs b/src/Migrator.Tests/RunnerSafetyTests.cs new file mode 100644 index 00000000..8a1900f4 --- /dev/null +++ b/src/Migrator.Tests/RunnerSafetyTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using DotNetProjects.Migrator.Providers; +using Microsoft.Data.Sqlite; +using NSubstitute; +using NUnit.Framework; +namespace Migrator.Tests; + +public class RunnerSafetyTests +{ + [Migration(1, Ignore = true)] public class One : Migration + { + public override void Up() => Database.ExecuteNonQuery("CREATE TABLE Example (Id INTEGER)"); + public override void Down() => Database.ExecuteNonQuery("DROP TABLE Example"); + } + [Migration(6, Ignore = true)] public class Six : One { } + [Migration(1, Scope = "other", Ignore = true)] public class Other : One { } + [Migration(2, Ignore = true)] public class Failure : Migration + { + public override void Up() => throw new InvalidOperationException("original"); + public override void Down() => throw new InvalidOperationException("original"); + } + [Test] public void LatestDoesNotDependOnRegistrationOrder() + { + var loader = new MigrationLoader(null, false, typeof(Six), typeof(One)); + Assert.That(loader.LastVersion, Is.EqualTo(6)); + } + [Test] public void PlanOrdersDowngradesThenMissingUpgrades() + { + var steps = MigrationPlanner.Create(new long[] { 4, 1, 3, 2 }, new long[] { 4, 1, 3 }, 2); + Assert.That(steps, Is.EqualTo(new[] { new MigrationStep(4, false), new MigrationStep(3, false), new MigrationStep(2, true) })); + } + [Test] public void MissingDowngradeIsRejected() + => Assert.Throws(() => MigrationPlanner.Create(new long[] { 1 }, new long[] { 2 }, 0)); + [Test] public void RollbackFailureDoesNotReplaceMigrationFailure() + { + var provider = Substitute.For(); + provider.AppliedMigrations.Returns(new List()); + provider.When(p => p.Rollback()).Do(_ => throw new Exception("rollback")); + var runner = new DotNetProjects.Migrator.Migrator(provider, false, typeof(Failure)); + var error = Assert.Throws(() => runner.MigrateTo(2)); + Assert.That(error.Message, Is.EqualTo("original")); + Assert.That(error.Data["RollbackException"], Is.TypeOf()); + provider.Received(1).Rollback(); + provider.DidNotReceive().Commit(); + } + [Test, Category("SQLite")] public void DryRunDoesNotCreateHistoryAndScopeIsolated() + { + using var connection = new SqliteConnection("Data Source=:memory:"); + connection.Open(); + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); + var runner = new DotNetProjects.Migrator.Migrator(provider, false, typeof(Other), typeof(One)) { DryRun = true }; + runner.MigrateToLastVersion(); + Assert.That(provider.TableExists("SchemaInfo"), Is.False); + Assert.That(provider.TableExists("Example"), Is.False); + Assert.That(((TransformationProvider)provider).HasActiveTransaction, Is.False); + runner.DryRun = false; + runner.MigrateToLastVersion(); + Assert.That(provider.AppliedMigrations, Is.EqualTo(new long[] { 1 })); + runner.MigrateTo(0); + Assert.That(provider.TableExists("Example"), Is.False); + Assert.That(provider.AppliedMigrations, Is.Empty); + } + [Test, Category("SQLite")] public void ForeignKeysAreRestoredAfterFailure() + { + using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True"); + connection.Open(); + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); + var runner = new DotNetProjects.Migrator.Migrator(provider, false, typeof(Failure)); + Assert.Throws(() => runner.MigrateTo(2)); + Assert.That(Convert.ToInt32(provider.ExecuteScalar("PRAGMA foreign_keys")), Is.EqualTo(1)); + Assert.That(((TransformationProvider)provider).HasActiveTransaction, Is.False); + Assert.That(provider.AppliedMigrations, Is.Empty); + } +} diff --git a/src/Migrator/Framework/IMigrationHistory.cs b/src/Migrator/Framework/IMigrationHistory.cs new file mode 100644 index 00000000..b3eb1bfc --- /dev/null +++ b/src/Migrator/Framework/IMigrationHistory.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +namespace DotNetProjects.Migrator.Framework; + +/// Optional provider contract for side-effect-free history inspection. +public interface IMigrationHistory +{ + string Scope { get; } + IReadOnlyList ReadAppliedMigrations(); + void InvalidateHistory(); +} diff --git a/src/Migrator/MigrateAnywhere.cs b/src/Migrator/MigrateAnywhere.cs index 2c374902..ae096a37 100644 --- a/src/Migrator/MigrateAnywhere.cs +++ b/src/Migrator/MigrateAnywhere.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers; @@ -19,7 +20,7 @@ public MigrateAnywhere(List availableMigrations, ITransformationProvider p _current = 0; if (provider.AppliedMigrations.Count > 0) { - _current = provider.AppliedMigrations[provider.AppliedMigrations.Count - 1]; + _current = provider.AppliedMigrations.Max(); } _goForward = false; } @@ -63,73 +64,9 @@ public override bool Continue(long version) public override void Migrate(IMigration migration) { -#if NETSTANDARD - var attr = migration.GetType().GetTypeInfo().GetCustomAttribute(); -#else - var attr = (MigrationAttribute)Attribute.GetCustomAttribute(migration.GetType(), typeof(MigrationAttribute)); -#endif - var foreignKeysWasOn = false; - if (_provider is SQLiteTransformationProvider sqlite) - { - foreignKeysWasOn = sqlite.IsPragmaForeignKeysOn(); - if (foreignKeysWasOn) - { - sqlite.SetPragmaForeignKeys(false); - } - } - - _provider.BeginTransaction(); - - if (_provider.AppliedMigrations.Contains(attr.Version)) - { - RemoveMigration(migration, attr); - } - else - { - ApplyMigration(migration, attr); - } - - if (foreignKeysWasOn && _provider is SQLiteTransformationProvider sqlite2) - { - sqlite2.SetPragmaForeignKeys(true); - } - } - - private void ApplyMigration(IMigration migration, MigrationAttribute attr) - { - // we're adding this one - _logger.MigrateUp(Current, migration.Name); - if (!DryRun) - { - var tProvider = _provider as TransformationProvider; - if (tProvider != null) - { - tProvider.CurrentMigration = migration; - } - - migration.Up(); - _provider.MigrationApplied(attr.Version, attr.Scope); - _provider.Commit(); - migration.AfterUp(); - } - } - - private void RemoveMigration(IMigration migration, MigrationAttribute attr) - { - // we're removing this one - _logger.MigrateDown(Current, migration.Name); - if (!DryRun) - { - var tProvider = _provider as TransformationProvider; - if (tProvider != null) - { - tProvider.CurrentMigration = migration; - } - - migration.Down(); - _provider.MigrationUnApplied(attr.Version, attr.Scope); - _provider.Commit(); - migration.AfterDown(); - } + if (DryRun) return; + var version = MigrationLoader.GetMigrationVersion(migration.GetType()); + MigrationExecution.Execute(_provider, migration, + new MigrationStep(version, !_provider.AppliedMigrations.Contains(version)), _logger); } } diff --git a/src/Migrator/MigrationExecution.cs b/src/Migrator/MigrationExecution.cs new file mode 100644 index 00000000..a233584b --- /dev/null +++ b/src/Migrator/MigrationExecution.cs @@ -0,0 +1,57 @@ +using System; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers; +using DotNetProjects.Migrator.Providers.Impl.SQLite; +namespace DotNetProjects.Migrator; + +internal static class MigrationExecution +{ + internal static void Execute(ITransformationProvider provider, IMigration migration, MigrationStep step, ILogger logger) + { + var concrete = provider as TransformationProvider; + if (concrete?.HasActiveTransaction == true) + throw new MigrationException("The runner cannot take ownership of an existing provider transaction."); + var sqlite = provider as SQLiteTransformationProvider; + var foreignKeys = sqlite?.IsPragmaForeignKeysOn() == true; + Exception failure = null; + var began = false; + try + { + if (foreignKeys) sqlite.SetPragmaForeignKeys(false); + provider.BeginTransaction(); + began = true; + if (concrete != null) concrete.CurrentMigration = migration; + if (step.IsUp) { logger.MigrateUp(step.Version, migration.Name); migration.Up(); } + else { logger.MigrateDown(step.Version, migration.Name); migration.Down(); } + if (sqlite != null && !sqlite.CheckForeignKeyIntegrity()) + throw new MigrationException("Migration would leave invalid SQLite foreign keys."); + if (step.IsUp) provider.MigrationApplied(step.Version, (provider as IMigrationHistory)?.Scope); + else provider.MigrationUnApplied(step.Version, (provider as IMigrationHistory)?.Scope); + provider.Commit(); + began = false; + } + catch (Exception ex) + { + failure = ex; + if (began) + { + try { provider.Rollback(); } + catch (Exception rollback) { ex.Data["RollbackException"] = rollback; } + } + logger.Exception(step.Version, migration.Name, ex); + throw; + } + finally + { + if (concrete != null) concrete.CurrentMigration = null; + try { if (foreignKeys) sqlite.SetPragmaForeignKeys(true); } + catch (Exception restore) + { + if (failure == null) throw; + failure.Data["ConnectionRestoreException"] = restore; + } + } + // These callbacks intentionally run after commit; failure cannot be rolled back. + if (step.IsUp) migration.AfterUp(); else migration.AfterDown(); + } +} diff --git a/src/Migrator/MigrationLoader.cs b/src/Migrator/MigrationLoader.cs index b3ef87bd..f6f6a933 100644 --- a/src/Migrator/MigrationLoader.cs +++ b/src/Migrator/MigrationLoader.cs @@ -1,170 +1,175 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Linq; -using DotNetProjects.Migrator.Framework; - -namespace DotNetProjects.Migrator; - -/// -/// Handles inspecting code to find all of the Migrations in assemblies and reading -/// other metadata such as the last revision, etc. -/// -public class MigrationLoader -{ - private readonly List _migrationsTypes = new List(); - private readonly ITransformationProvider _provider; - - public MigrationLoader(ITransformationProvider provider, Assembly migrationAssembly, bool trace) - { - _provider = provider; - AddMigrations(migrationAssembly); - - if (trace) - { - provider.Logger.Trace("Loaded migrations:"); - foreach (var t in _migrationsTypes) - { - provider.Logger.Trace("{0} {1}", GetMigrationVersion(t).ToString().PadLeft(5), StringUtils.ToHumanName(t.Name)); - } - } - } - - public MigrationLoader(ITransformationProvider provider, bool trace, params Type[] migrationTypes) - { - _provider = provider; - _migrationsTypes.AddRange(migrationTypes); - - if (trace) - { - provider.Logger.Trace("Loaded migrations:"); - foreach (var t in _migrationsTypes) - { - provider.Logger.Trace("{0} {1}", GetMigrationVersion(t).ToString().PadLeft(5), StringUtils.ToHumanName(t.Name)); - } - } - } - - /// - /// Returns registered migration types. - /// - public virtual List MigrationsTypes - { - get { return _migrationsTypes; } - } - - /// - /// Returns the last version of the migrations. - /// - public virtual long LastVersion - { - get - { - if (_migrationsTypes.Count == 0) - { - return 0; - } - - return GetMigrationVersion(_migrationsTypes[_migrationsTypes.Count - 1]); - } - } - - public virtual void AddMigrations(Assembly migrationAssembly) - { - if (migrationAssembly != null) - { - _migrationsTypes.AddRange(GetMigrationTypes(migrationAssembly)); - } - } - - /// - /// Check for duplicated version in migrations. - /// - /// CheckForDuplicatedVersion - public virtual void CheckForDuplicatedVersion() - { - var versions = new List(); - foreach (var t in _migrationsTypes) - { - var version = GetMigrationVersion(t); - - if (versions.Contains(version)) - { - throw new DuplicatedVersionException(version); - } - - versions.Add(version); - } - } - - /// - /// Collect migrations in one Assembly. - /// - /// The Assembly to browse. - /// The migrations collection - public static List GetMigrationTypes(Assembly asm) - { - var migrations = new List(); - foreach (var t in asm.GetExportedTypes()) - { - - -#if NETSTANDARD - var attrib = t.GetTypeInfo().GetCustomAttribute(); - if (attrib != null && typeof(IMigration).GetTypeInfo().IsAssignableFrom(t) && !attrib.Ignore) - { - migrations.Add(t); - } -#else - var attrib = (MigrationAttribute)Attribute.GetCustomAttribute(t, typeof(MigrationAttribute)); - if (attrib != null && typeof(IMigration).IsAssignableFrom(t) && !attrib.Ignore) - { - migrations.Add(t); - } -#endif - - - } - - migrations.Sort(new MigrationTypeComparer(true)); - return migrations; - } - - /// - /// Returns the version of the migration - /// MigrationAttribute. - /// - /// Migration type. - /// Version number sepcified in the attribute - public static long GetMigrationVersion(Type t) - { - var attrib = (MigrationAttribute)Attribute.GetCustomAttribute(t, typeof(MigrationAttribute)); - return attrib.Version; - } - - public List GetAvailableMigrations() - { - _migrationsTypes.Sort(new MigrationTypeComparer(true)); - return _migrationsTypes.Select(x => GetMigrationVersion(x)).ToList(); - } - - public virtual IMigration GetMigration(long version) - { - foreach (var t in _migrationsTypes) - { - if (GetMigrationVersion(t) == version) - { - var migration = CreateInstance(t); - migration.Database = _provider; - return migration; - } - } - - return null; - } - - public virtual IMigration CreateInstance(Type migrationType) - { - return (IMigration)Activator.CreateInstance(migrationType); - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers; + +namespace DotNetProjects.Migrator; + +/// +/// Handles inspecting code to find all of the Migrations in assemblies and reading +/// other metadata such as the last revision, etc. +/// +public class MigrationLoader +{ + private readonly List _migrationsTypes = new List(); + private readonly ITransformationProvider _provider; + + public MigrationLoader(ITransformationProvider provider, Assembly migrationAssembly, bool trace) + { + _provider = provider; + AddMigrations(migrationAssembly); + + if (trace) + { + provider.Logger.Trace("Loaded migrations:"); + foreach (var t in _migrationsTypes) + { + provider.Logger.Trace("{0} {1}", GetMigrationVersion(t).ToString().PadLeft(5), StringUtils.ToHumanName(t.Name)); + } + } + } + + public MigrationLoader(ITransformationProvider provider, bool trace, params Type[] migrationTypes) + { + _provider = provider; + _migrationsTypes.AddRange(migrationTypes); + + if (trace) + { + provider.Logger.Trace("Loaded migrations:"); + foreach (var t in _migrationsTypes) + { + provider.Logger.Trace("{0} {1}", GetMigrationVersion(t).ToString().PadLeft(5), StringUtils.ToHumanName(t.Name)); + } + } + } + + /// + /// Returns registered migration types. + /// + public virtual List MigrationsTypes + { + get { return _migrationsTypes; } + } + + /// + /// Returns the last version of the migrations. + /// + public virtual long LastVersion + { + get + { + if (_migrationsTypes.Count == 0) + { + return 0; + } + + return SelectedTypes.Select(GetMigrationVersion).DefaultIfEmpty(0).Max(); + } + } + + public IEnumerable SelectedTypes => _migrationsTypes.Where(t => + t.GetCustomAttribute()?.Scope is not string scope || + scope == (_provider as IMigrationHistory)?.Scope); + + public virtual void AddMigrations(Assembly migrationAssembly) + { + if (migrationAssembly != null) + { + _migrationsTypes.AddRange(GetMigrationTypes(migrationAssembly)); + } + } + + /// + /// Check for duplicated version in migrations. + /// + /// CheckForDuplicatedVersion + public virtual void CheckForDuplicatedVersion() + { + var versions = new List(); + foreach (var t in SelectedTypes) + { + var version = GetMigrationVersion(t); + + if (versions.Contains(version)) + { + throw new DuplicatedVersionException(version); + } + + versions.Add(version); + } + } + + /// + /// Collect migrations in one Assembly. + /// + /// The Assembly to browse. + /// The migrations collection + public static List GetMigrationTypes(Assembly asm) + { + var migrations = new List(); + foreach (var t in asm.GetExportedTypes()) + { + + +#if NETSTANDARD + var attrib = t.GetTypeInfo().GetCustomAttribute(); + if (attrib != null && typeof(IMigration).GetTypeInfo().IsAssignableFrom(t) && !attrib.Ignore) + { + migrations.Add(t); + } +#else + var attrib = (MigrationAttribute)Attribute.GetCustomAttribute(t, typeof(MigrationAttribute)); + if (attrib != null && typeof(IMigration).IsAssignableFrom(t) && !attrib.Ignore) + { + migrations.Add(t); + } +#endif + + + } + + migrations.Sort(new MigrationTypeComparer(true)); + return migrations; + } + + /// + /// Returns the version of the migration + /// MigrationAttribute. + /// + /// Migration type. + /// Version number sepcified in the attribute + public static long GetMigrationVersion(Type t) + { + var attrib = (MigrationAttribute)Attribute.GetCustomAttribute(t, typeof(MigrationAttribute)); + return attrib?.Version ?? throw new ArgumentException($"{t.FullName} has no Migration attribute."); + } + + public List GetAvailableMigrations() + { + _migrationsTypes.Sort(new MigrationTypeComparer(true)); + return SelectedTypes.Select(GetMigrationVersion).ToList(); + } + + public virtual IMigration GetMigration(long version) + { + foreach (var t in SelectedTypes) + { + if (GetMigrationVersion(t) == version) + { + var migration = CreateInstance(t); + migration.Database = _provider; + return migration; + } + } + + return null; + } + + public virtual IMigration CreateInstance(Type migrationType) + { + return (IMigration)Activator.CreateInstance(migrationType); + } +} diff --git a/src/Migrator/MigrationPlan.cs b/src/Migrator/MigrationPlan.cs new file mode 100644 index 00000000..b172b59e --- /dev/null +++ b/src/Migrator/MigrationPlan.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DotNetProjects.Migrator.Framework; +namespace DotNetProjects.Migrator; + +public sealed record MigrationStep(long Version, bool IsUp); + +public static class MigrationPlanner +{ + public static IReadOnlyList Create(IEnumerable available, IEnumerable applied, long target) + { + if (target < 0) throw new ArgumentOutOfRangeException(nameof(target)); + var versions = available.ToArray(); + var duplicate = versions.GroupBy(v => v).FirstOrDefault(g => g.Count() > 1); + if (duplicate != null) throw new DuplicatedVersionException(duplicate.Key); + if (versions.Any(v => v <= 0)) throw new MigrationException("Migration versions must be positive."); + var history = applied.ToHashSet(); + var missing = history.Where(v => v > target && !versions.Contains(v)).ToArray(); + if (missing.Length != 0) throw new MigrationException("Missing downgrade migrations: " + string.Join(", ", missing)); + return history.Where(v => v > target).OrderByDescending(v => v).Select(v => new MigrationStep(v, false)) + .Concat(versions.Where(v => v <= target && !history.Contains(v)).OrderBy(v => v).Select(v => new MigrationStep(v, true))) + .ToArray(); + } +} diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index 46fbd20b..e9a96b67 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -196,53 +196,43 @@ public void MigrateToLastVersion() /// If dryrun is set, don't write any changes to the database. /// /// The version that must became the current one - public void MigrateTo(long version) + public IReadOnlyList Plan(long version) { - if (_migrationLoader.MigrationsTypes.Count == 0) - { - _logger.Warn("No public classes with the Migration attribute were found."); - return; - } + _migrationLoader.CheckForDuplicatedVersion(); + if (_provider is not IMigrationHistory history) + throw new NotSupportedException("Read-only planning requires IMigrationHistory on custom providers."); + return MigrationPlanner.Create(_migrationLoader.GetAvailableMigrations(), history.ReadAppliedMigrations(), version); + } + public void MigrateTo(long version) + { + _migrationLoader.CheckForDuplicatedVersion(); + var history = _provider is IMigrationHistory reader + ? reader.ReadAppliedMigrations().ToList() + : DryRun ? throw new NotSupportedException("DryRun requires IMigrationHistory on custom providers.") + : new List(_provider.AppliedMigrations); + var plan = MigrationPlanner.Create(_migrationLoader.GetAvailableMigrations(), history, version); + Logger.Started(history, version); var firstRun = true; - var migrate = BaseMigrate.GetInstance(_migrationLoader.GetAvailableMigrations(), _provider, _logger); - migrate.DryRun = DryRun; - Logger.Started(migrate.AppliedVersions, version); - - while (migrate.Continue(version)) + foreach (var step in plan) { - var migration = _migrationLoader.GetMigration(migrate.Current); - if (null == migration) + if (DryRun) { - _logger.Skipping(migrate.Current); - migrate.Iterate(); + if (step.IsUp) Logger.MigrateUp(step.Version, "Preview"); + else Logger.MigrateDown(step.Version, "Preview"); continue; } - - try - { - if (firstRun) - { - migration.InitializeOnce(_args); - firstRun = false; - } - - migrate.Migrate(migration); - } - catch (Exception ex) + var migration = _migrationLoader.GetMigration(step.Version); + if (firstRun) { - Logger.Exception(migrate.Current, migration.Name, ex); - - // Oho! error! We rollback changes. - Logger.RollingBack(migrate.Previous); - _provider.Rollback(); - - throw; + migration.InitializeOnce(_args); + firstRun = false; } - - migrate.Iterate(); + MigrationExecution.Execute(_provider, migration, step, Logger); + if (step.IsUp) history.Add(step.Version); + else history.Remove(step.Version); } - - Logger.Finished(migrate.AppliedVersions, version); + history.Sort(); + Logger.Finished(history, version); } } diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index f8ce3827..381aeaba 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -85,7 +85,7 @@ public override void AddForeignKey( Name = name, ParentColumns = parentColumns, ParentTable = parentTable, - OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(constraint), + OnDelete = new ForeignKeyConstraintMapper().SqlForConstraint(constraint), }; sqliteTableInfo.ForeignKeys @@ -787,7 +787,6 @@ public SQLiteTableInfo GetSQLiteTableInfo(string tableName) public bool CheckForeignKeyIntegrity() { - ExecuteNonQuery("PRAGMA foreign_keys = ON"); using var cmd = CreateCommand(); using var reader = ExecuteQuery(cmd, "PRAGMA foreign_key_check"); @@ -815,7 +814,7 @@ public void SetPragmaForeignKeys(bool isOn) var onOffString = isOn ? "ON" : "OFF"; using var cmd = CreateCommand(); - ExecuteQuery(cmd, $"PRAGMA foreign_keys = {onOffString}"); + ExecuteNonQuery($"PRAGMA foreign_keys = {onOffString}"); } public void RecreateTable(SQLiteTableInfo sqliteTableInfo) @@ -1458,13 +1457,13 @@ public override void AddTable(string name, string engine, params IDbField[] fiel throw new Exception("No foreign key constraint name given"); } - var foreignKeySql = $"CONSTRAINT {fk.Name} FOREIGN KEY ({sourceColumnNamesQuotedString}) REFERENCES {parentTableNameQuoted}({parentColumnNamesQuotedString})"; - if (!string.IsNullOrWhiteSpace(fk.OnDelete) && !string.Equals(fk.OnDelete, "NO ACTION", StringComparison.OrdinalIgnoreCase)) - { - foreignKeySql += $" ON DELETE {fk.OnDelete}"; - } - - foreignKeyStrings.Add(foreignKeySql); + var foreignKeySql = $"CONSTRAINT {fk.Name} FOREIGN KEY ({sourceColumnNamesQuotedString}) REFERENCES {parentTableNameQuoted}({parentColumnNamesQuotedString})"; + if (!string.IsNullOrWhiteSpace(fk.OnDelete) && !string.Equals(fk.OnDelete, "NO ACTION", StringComparison.OrdinalIgnoreCase)) + { + foreignKeySql += $" ON DELETE {fk.OnDelete}"; + } + + foreignKeyStrings.Add(foreignKeySql); } if (foreignKeyStrings.Count > 0) diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 7b766915..dd43133f 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -34,7 +34,7 @@ namespace DotNetProjects.Migrator.Providers; /// Base class for every transformation providers. /// A 'tranformation' is an operation that modifies the database. /// -public abstract class TransformationProvider : ITransformationProvider +public abstract class TransformationProvider : ITransformationProvider, IMigrationHistory { private string _scope; protected readonly string _connectionString; @@ -78,6 +78,7 @@ public string SchemaInfoTable set { _schemaInfotable = value; + InvalidateHistory(); } } @@ -1560,85 +1561,71 @@ public virtual void BeginTransaction() if (_transaction == null && _connection != null) { EnsureHasConnection(); - _transaction = _connection.BeginTransaction(IsolationLevel.ReadCommitted); + _transaction = _connection.BeginTransaction(_dialect is DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect ? IsolationLevel.Serializable : IsolationLevel.ReadCommitted); } } /// /// Rollback the current migration. Called by the migration mediator. /// - public virtual void Rollback() + public virtual void Rollback() => CompleteTransaction(false); + + public virtual void Commit() => CompleteTransaction(true); + + public bool HasActiveTransaction => _transaction != null; + public string Scope => _scope; + public void InvalidateHistory() => _appliedMigrations = null; + + private void CompleteTransaction(bool commit) { - if (_transaction != null && _connection != null && _connection.State == ConnectionState.Open) + var transaction = _transaction; + _transaction = null; + try { - try + if (transaction != null) { - _transaction.Rollback(); - } - finally - { - if (!_outsideConnection) - { - _connection.Close(); - } + if (commit) transaction.Commit(); + else transaction.Rollback(); } } - _transaction = null; + finally + { + try { transaction?.Dispose(); } + finally { InvalidateHistory(); } + } } - /// - /// Commit the current transaction. Called by the migrations mediator. - /// - public virtual void Commit() + /// Reads existing history without creating or upgrading its table. + public virtual IReadOnlyList ReadAppliedMigrations() { - if (_transaction != null && _connection != null && _connection.State == ConnectionState.Open) + var versions = new List(); + if (!TableExists(_schemaInfotable)) return versions; + var hasScope = ColumnExists(_schemaInfotable, "Scope"); + if (!hasScope && _scope != "default") return versions; + using var cmd = CreateCommand(); + var predicate = "1=1"; + if (hasScope) { - try - { - _transaction.Commit(); - } - finally - { - if (!_outsideConnection) - { - _connection.Close(); - } - } + var parameter = cmd.CreateParameter(); + parameter.ParameterName = GenerateParameterNameParameter(0); + parameter.Value = _scope; + cmd.Parameters.Add(parameter); + predicate = QuoteColumnNameIfRequired("Scope") + " = " + GenerateParameterName(0); } - _transaction = null; + using var reader = Select(cmd, QuoteColumnNameIfRequired("Version"), QuoteTableNameIfRequired(_schemaInfotable), predicate); + while (reader.Read()) versions.Add(Convert.ToInt64(reader.GetValue(0))); + versions.Sort(); + return versions; } - /// - /// The list of Migrations currently applied to the database. - /// public virtual List AppliedMigrations { get { if (_appliedMigrations == null) { - _appliedMigrations = new List(); - CreateSchemaInfoTable(); - - var versionColumn = "Version"; - var scopeColumn = "Scope"; - - versionColumn = QuoteColumnNameIfRequired(versionColumn); - scopeColumn = QuoteColumnNameIfRequired(scopeColumn); - - using var cmd = CreateCommand(); - using var reader = Select(cmd, versionColumn, _schemaInfotable, string.Format("{0} = '{1}'", scopeColumn, _scope)); - while (reader.Read()) - { - if (reader.GetFieldType(0) == typeof(decimal)) - { - _appliedMigrations.Add((long)reader.GetDecimal(0)); - } - else - { - _appliedMigrations.Add(reader.GetInt64(0)); - } - } + CreateSchemaInfoTable(); // Preserve the legacy property contract. + _appliedMigrations = new List(ReadAppliedMigrations()); } return _appliedMigrations; } @@ -1658,7 +1645,7 @@ public virtual void MigrationApplied(long version, string scope) { CreateSchemaInfoTable(); Insert(_schemaInfotable, ["Scope", "Version", "TimeStamp"], [scope ?? _scope, version, DateTime.UtcNow]); - _appliedMigrations.Add(version); + InvalidateHistory(); } /// @@ -1669,7 +1656,7 @@ public virtual void MigrationUnApplied(long version, string scope) { CreateSchemaInfoTable(); Delete(_schemaInfotable, ["Scope", "Version"], [scope ?? _scope, version]); - _appliedMigrations.Remove(version); + InvalidateHistory(); } public virtual void AddColumn(string table, Column column) @@ -1702,23 +1689,13 @@ public virtual void ExecuteSchemaBuilder(SchemaBuilder builder) public void Dispose() { - if (_connection != null && _connection.State == ConnectionState.Open) + try { if (_transaction != null) Rollback(); } + finally { - if (!_outsideConnection) - { - _connection.Close(); - } + if (!_outsideConnection) _connection?.Dispose(); + _connection = null; + InvalidateHistory(); } - - if (_connection != null) - { - if (!_outsideConnection) - { - _connection.Close(); - } - } - - _connection = null; } public virtual string QuoteColumnNameIfRequired(string name) From ce8f79d8c2c7673aa77edd707d7dea4ad396b0b7 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 14:37:55 +0200 Subject: [PATCH 2/6] Preserve legacy scope handling and recover failed commits The PR matrix exposed Firebird history creation occurring inside the first migration transaction. Initialize history before real execution while keeping planning read-only. Preserve explicitly scoped migrations and their history arguments for custom providers without IMigrationHistory. Retain failed transactions so rollback can recover a failed commit. Add regression tests for legacy scopes and transaction recovery, and enable CI on stacked codex PR bases. Validation: targeted runner tests; full provider matrix reruns on push. --- .github/workflows/dotnetpull.yml | 168 +++++++++--------- src/Migrator.Tests/RunnerSafetyTests.cs | 27 +++ src/Migrator/MigrationExecution.cs | 5 +- src/Migrator/MigrationLoader.cs | 4 +- src/Migrator/Migrator.cs | 6 +- .../Providers/TransformationProvider.cs | 9 +- 6 files changed, 122 insertions(+), 97 deletions(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 5e9af897..d9fef12e 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -1,84 +1,84 @@ -name: .NET Pull Request -on: - push: - branches: [master] - pull_request: - branches: [master] - workflow_dispatch: -permissions: - contents: read -concurrency: - group: live-databases-${{ github.ref }} - cancel-in-progress: true -jobs: - test: - name: Test (${{ matrix.database }}) - runs-on: ubuntu-22.04 - timeout-minutes: 35 - strategy: - fail-fast: false - matrix: - database: [Unit, SQLite, SQLServer, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - name: Start database - shell: bash - run: | - mkdir -p TestResults - bash .github/scripts/start-database.sh "${{ matrix.database }}" 2>&1 | tee TestResults/startup.log - timeout-minutes: 15 - - name: Build - run: dotnet build Migrator.slnx -p:LiveDatabase=${{ matrix.database }} - - name: Configure native IBM drivers - if: matrix.database == 'Db2' || matrix.database == 'Informix' - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y libaio1 libxml2 unixodbc libncurses5 - output="$GITHUB_WORKSPACE/src/Migrator.Tests/bin/Debug/net9.0" - if [ "${{ matrix.database }}" = Db2 ]; then - echo "DB2_CLI_DRIVER_INSTALL_PATH=$output/clidriver" >> "$GITHUB_ENV" - echo "LD_LIBRARY_PATH=$output/clidriver/lib" >> "$GITHUB_ENV" - else - echo "DELIMIDENT=y" >> "$GITHUB_ENV" - echo "INFORMIXDIR=$output/native" >> "$GITHUB_ENV" - echo "LD_LIBRARY_PATH=$output/native/lib:$output/native/lib/cli:$output/native/lib/esql" >> "$GITHUB_ENV" - fi - - name: Test - shell: pwsh - run: ./.github/scripts/test.ps1 -Database ${{ matrix.database }} - - name: Collect database logs - if: always() - run: | - mkdir -p TestResults - if docker inspect migrator-db >/dev/null 2>&1; then - docker logs migrator-db > TestResults/database.log 2>&1 - docker inspect migrator-db > TestResults/container.json - fi - - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-results-${{ matrix.database }} - path: TestResults/ - if-no-files-found: error - - name: Remove test container - if: always() - run: | - if docker inspect migrator-db >/dev/null 2>&1; then - docker rm -fv migrator-db - fi - coverage: - name: Verify complete test coverage - needs: test - runs-on: ubuntu-22.04 - timeout-minutes: 5 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - pattern: test-results-* - path: TestResults - - run: python3 .github/scripts/verify-test-coverage.py TestResults +name: .NET Pull Request +on: + push: + branches: [master] + pull_request: + branches: [master, "codex/**"] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: live-databases-${{ github.ref }} + cancel-in-progress: true +jobs: + test: + name: Test (${{ matrix.database }}) + runs-on: ubuntu-22.04 + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + database: [Unit, SQLite, SQLServer, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Start database + shell: bash + run: | + mkdir -p TestResults + bash .github/scripts/start-database.sh "${{ matrix.database }}" 2>&1 | tee TestResults/startup.log + timeout-minutes: 15 + - name: Build + run: dotnet build Migrator.slnx -p:LiveDatabase=${{ matrix.database }} + - name: Configure native IBM drivers + if: matrix.database == 'Db2' || matrix.database == 'Informix' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y libaio1 libxml2 unixodbc libncurses5 + output="$GITHUB_WORKSPACE/src/Migrator.Tests/bin/Debug/net9.0" + if [ "${{ matrix.database }}" = Db2 ]; then + echo "DB2_CLI_DRIVER_INSTALL_PATH=$output/clidriver" >> "$GITHUB_ENV" + echo "LD_LIBRARY_PATH=$output/clidriver/lib" >> "$GITHUB_ENV" + else + echo "DELIMIDENT=y" >> "$GITHUB_ENV" + echo "INFORMIXDIR=$output/native" >> "$GITHUB_ENV" + echo "LD_LIBRARY_PATH=$output/native/lib:$output/native/lib/cli:$output/native/lib/esql" >> "$GITHUB_ENV" + fi + - name: Test + shell: pwsh + run: ./.github/scripts/test.ps1 -Database ${{ matrix.database }} + - name: Collect database logs + if: always() + run: | + mkdir -p TestResults + if docker inspect migrator-db >/dev/null 2>&1; then + docker logs migrator-db > TestResults/database.log 2>&1 + docker inspect migrator-db > TestResults/container.json + fi + - uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-${{ matrix.database }} + path: TestResults/ + if-no-files-found: error + - name: Remove test container + if: always() + run: | + if docker inspect migrator-db >/dev/null 2>&1; then + docker rm -fv migrator-db + fi + coverage: + name: Verify complete test coverage + needs: test + runs-on: ubuntu-22.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: test-results-* + path: TestResults + - run: python3 .github/scripts/verify-test-coverage.py TestResults diff --git a/src/Migrator.Tests/RunnerSafetyTests.cs b/src/Migrator.Tests/RunnerSafetyTests.cs index 8a1900f4..e5b530ea 100644 --- a/src/Migrator.Tests/RunnerSafetyTests.cs +++ b/src/Migrator.Tests/RunnerSafetyTests.cs @@ -24,6 +24,33 @@ public class RunnerSafetyTests public override void Up() => throw new InvalidOperationException("original"); public override void Down() => throw new InvalidOperationException("original"); } + [Test] public void CustomProvidersRetainExplicitMigrationScope() + { + var provider = Substitute.For(); + provider.AppliedMigrations.Returns(new List()); + var runner = new DotNetProjects.Migrator.Migrator(provider, false, typeof(Other)); + Assert.That(runner.AssemblyLastMigrationVersion, Is.EqualTo(1)); + runner.MigrateToLastVersion(); + provider.Received().MigrationApplied(1, "other"); + } + [Test] public void FailedCommitRetainsTransactionForRollback() + { + var connection = Substitute.For(); + var transaction = Substitute.For(); + connection.State.Returns(System.Data.ConnectionState.Open); + connection.BeginTransaction(System.Data.IsolationLevel.Serializable).Returns(transaction); + transaction.When(t => t.Commit()).Do(_ => throw new InvalidOperationException("commit")); + using var provider = new TransactionTestProvider(connection); + provider.BeginTransaction(); + Assert.Throws(() => provider.Commit()); + Assert.That(provider.HasActiveTransaction, Is.True); + provider.Rollback(); + transaction.Received(1).Rollback(); + transaction.Received(1).Dispose(); + Assert.That(provider.HasActiveTransaction, Is.False); + } + private sealed class TransactionTestProvider(System.Data.IDbConnection connection) + : TransformationProvider(new DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect(), connection, null, "default"); [Test] public void LatestDoesNotDependOnRegistrationOrder() { var loader = new MigrationLoader(null, false, typeof(Six), typeof(One)); diff --git a/src/Migrator/MigrationExecution.cs b/src/Migrator/MigrationExecution.cs index a233584b..ef5a3421 100644 --- a/src/Migrator/MigrationExecution.cs +++ b/src/Migrator/MigrationExecution.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers; using DotNetProjects.Migrator.Providers.Impl.SQLite; @@ -25,8 +26,8 @@ internal static void Execute(ITransformationProvider provider, IMigration migrat else { logger.MigrateDown(step.Version, migration.Name); migration.Down(); } if (sqlite != null && !sqlite.CheckForeignKeyIntegrity()) throw new MigrationException("Migration would leave invalid SQLite foreign keys."); - if (step.IsUp) provider.MigrationApplied(step.Version, (provider as IMigrationHistory)?.Scope); - else provider.MigrationUnApplied(step.Version, (provider as IMigrationHistory)?.Scope); + if (step.IsUp) provider.MigrationApplied(step.Version, migration.GetType().GetCustomAttribute()?.Scope ?? (provider as IMigrationHistory)?.Scope); + else provider.MigrationUnApplied(step.Version, migration.GetType().GetCustomAttribute()?.Scope ?? (provider as IMigrationHistory)?.Scope); provider.Commit(); began = false; } diff --git a/src/Migrator/MigrationLoader.cs b/src/Migrator/MigrationLoader.cs index f6f6a933..522b7b36 100644 --- a/src/Migrator/MigrationLoader.cs +++ b/src/Migrator/MigrationLoader.cs @@ -71,8 +71,8 @@ public virtual long LastVersion } public IEnumerable SelectedTypes => _migrationsTypes.Where(t => - t.GetCustomAttribute()?.Scope is not string scope || - scope == (_provider as IMigrationHistory)?.Scope); + _provider is not IMigrationHistory history || + t.GetCustomAttribute()?.Scope is not string scope || scope == history.Scope); public virtual void AddMigrations(Assembly migrationAssembly) { diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index e9a96b67..cd16c046 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -207,9 +207,9 @@ public IReadOnlyList Plan(long version) public void MigrateTo(long version) { _migrationLoader.CheckForDuplicatedVersion(); - var history = _provider is IMigrationHistory reader - ? reader.ReadAppliedMigrations().ToList() - : DryRun ? throw new NotSupportedException("DryRun requires IMigrationHistory on custom providers.") + var history = DryRun + ? _provider is IMigrationHistory reader ? reader.ReadAppliedMigrations().ToList() + : throw new NotSupportedException("DryRun requires IMigrationHistory on custom providers.") : new List(_provider.AppliedMigrations); var plan = MigrationPlanner.Create(_migrationLoader.GetAvailableMigrations(), history, version); Logger.Started(history, version); diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index dd43133f..0d0fc72b 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -1579,20 +1579,17 @@ public virtual void BeginTransaction() private void CompleteTransaction(bool commit) { var transaction = _transaction; - _transaction = null; try { if (transaction != null) { if (commit) transaction.Commit(); else transaction.Rollback(); + _transaction = null; + transaction.Dispose(); } } - finally - { - try { transaction?.Dispose(); } - finally { InvalidateHistory(); } - } + finally { InvalidateHistory(); } } /// Reads existing history without creating or upgrading its table. From 9cde33e8c8e690490bfd1b0ed57cb77c1d8f5e28 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 14:38:58 +0200 Subject: [PATCH 3/6] Complete the transaction recovery test provider Implement the three abstract metadata members on the isolated transaction test provider. This keeps the failed-commit recovery test independent of live database metadata. Validation: 31 targeted runner, loader and rollback tests passed. --- src/Migrator.Tests/RunnerSafetyTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Migrator.Tests/RunnerSafetyTests.cs b/src/Migrator.Tests/RunnerSafetyTests.cs index e5b530ea..e5fe5898 100644 --- a/src/Migrator.Tests/RunnerSafetyTests.cs +++ b/src/Migrator.Tests/RunnerSafetyTests.cs @@ -50,7 +50,12 @@ [Test] public void FailedCommitRetainsTransactionForRollback() Assert.That(provider.HasActiveTransaction, Is.False); } private sealed class TransactionTestProvider(System.Data.IDbConnection connection) - : TransformationProvider(new DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect(), connection, null, "default"); + : TransformationProvider(new DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect(), connection, null, "default") + { + public override List GetDatabases() => new(); + public override bool ConstraintExists(string table, string name) => false; + public override bool IndexExists(string table, string name) => false; + } [Test] public void LatestDoesNotDependOnRegistrationOrder() { var loader = new MigrationLoader(null, false, typeof(Six), typeof(One)); From 96c9e1d57b5695b7a27644cd3a16074238e04ea9 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:26:00 +0200 Subject: [PATCH 4/6] Make date-version rollback tests observe the actual failure and rollback Replace broad try/catch blocks that swallowed Assert.Fail with exact exception assertions, verify the migration's error message and assert Rollback was called immediately after execution. Remove never-triggered Dispose callback assertions from loader setup and date-version fixtures. Validation: solution build and Unit 68 passed. --- src/Migrator.Tests/MigrationLoaderTest.cs | 16 +- src/Migrator.Tests/MigratorTestDates.cs | 589 +++++++++++----------- 2 files changed, 286 insertions(+), 319 deletions(-) diff --git a/src/Migrator.Tests/MigrationLoaderTest.cs b/src/Migrator.Tests/MigrationLoaderTest.cs index 7394d1f0..b7c1d641 100644 --- a/src/Migrator.Tests/MigrationLoaderTest.cs +++ b/src/Migrator.Tests/MigrationLoaderTest.cs @@ -15,30 +15,18 @@ public class MigrationLoaderTest [SetUp] public void SetUp() { - SetUpCurrentVersion(0, false); + SetUpLoader(); } #endregion private MigrationLoader _migrationLoader; - private void SetUpCurrentVersion(int version, bool assertRollbackIsCalled) + private void SetUpLoader() { var providerMock = Substitute.For(); providerMock.Logger = new Logger(false); - providerMock.When(x => x.Dispose()).Do(_ => - { - if (assertRollbackIsCalled) - { - providerMock.Received().Rollback(); - } - else - { - providerMock.DidNotReceive().Rollback(); - } - }); - _migrationLoader = new MigrationLoader(providerMock, Assembly.GetExecutingAssembly(), true); _migrationLoader.MigrationsTypes.Clear(); _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.FirstMigration)); diff --git a/src/Migrator.Tests/MigratorTestDates.cs b/src/Migrator.Tests/MigratorTestDates.cs index 0065ef3f..3660abbc 100644 --- a/src/Migrator.Tests/MigratorTestDates.cs +++ b/src/Migrator.Tests/MigratorTestDates.cs @@ -1,305 +1,284 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using DotNetProjects.Migrator; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Loggers; -using NSubstitute; -using NUnit.Framework; - -namespace Migrator.Tests; - -[TestFixture] -public class MigratorTestDates -{ - [SetUp] - public void SetUp() - { - SetUpCurrentVersion(0); - } - - private DotNetProjects.Migrator.Migrator _migrator; - - // Collections that contain the version that are called migrating up and down - private static readonly List _upCalled = []; - private static readonly List _downCalled = []; - - private void SetUpCurrentVersion(long version) - { - SetUpCurrentVersion(version, false); - } - - private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled) - { - SetUpCurrentVersion(version, assertRollbackIsCalled, true); - } - - private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled, bool includeBad) - { - var appliedVersions = new List(); - - for (long i = 2008010195; i <= version; i += 10000) - { - appliedVersions.Add(i); - } - - SetUpCurrentVersion(version, appliedVersions, assertRollbackIsCalled, includeBad); - } - - private void SetUpCurrentVersion(long version, List appliedVersions, bool assertRollbackIsCalled, bool includeBad) - { - var providerMock = Substitute.For(); - - providerMock.AppliedMigrations.Returns(appliedVersions); - providerMock.Logger.Returns(new Logger(false)); - - providerMock.When(x => x.Dispose()).Do(_ => - { - if (assertRollbackIsCalled) - { - providerMock.Received().Rollback(); - } - else - { - providerMock.DidNotReceive().Rollback(); - } - }); - - _migrator = new DotNetProjects.Migrator.Migrator((ITransformationProvider)providerMock, Assembly.GetExecutingAssembly(), false); - - _migrator.MigrationsTypes.Clear(); - _upCalled.Clear(); - _downCalled.Clear(); - - _migrator.MigrationsTypes.Add(typeof(FirstMigration)); - _migrator.MigrationsTypes.Add(typeof(SecondMigration)); - _migrator.MigrationsTypes.Add(typeof(ThirdMigration)); - _migrator.MigrationsTypes.Add(typeof(FourthMigration)); - _migrator.MigrationsTypes.Add(typeof(SixthMigration)); - - if (includeBad) - { - _migrator.MigrationsTypes.Add(typeof(BadMigration)); - } - } - - public class AbstractTestMigration : Migration - { - public override void Up() - { - _upCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); - } - - public override void Down() - { - _downCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); - } - } - - [Migration(2008010195, Ignore = true)] - public class FirstMigration : AbstractTestMigration - { - } - - [Migration(2008020195, Ignore = true)] - public class SecondMigration : AbstractTestMigration - { - } - - [Migration(2008030195, Ignore = true)] - public class ThirdMigration : AbstractTestMigration - { - } - - [Migration(2008040195, Ignore = true)] - public class FourthMigration : AbstractTestMigration - { - } - - [Migration(2008050195, Ignore = true)] - public class BadMigration : AbstractTestMigration - { - public override void Up() - { - throw new Exception("oh uh!"); - } - - public override void Down() - { - throw new Exception("oh uh!"); - } - } - - [Migration(2008060195, Ignore = true)] - public class SixthMigration : AbstractTestMigration - { - } - - [Migration(2008070195)] - public class NonIgnoredMigration : AbstractTestMigration - { - } - - [Test] - public void MigrateBackward() - { - SetUpCurrentVersion(2008030195); - _migrator.MigrateTo(2008010195); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(2, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008030195, Is.EqualTo(_downCalled[0])); - Assert.That(2008020195, Is.EqualTo(_downCalled[1])); - } - - [Test] - public void MigrateDownWithHoles() - { - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008030195); - migs.Add(2008040195); - SetUpCurrentVersion(2008040195, migs, false, false); - _migrator.MigrateTo(2008030195); - - Assert.That(1, Is.EqualTo(_upCalled.Count)); - Assert.That(1, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008020195, Is.EqualTo(_upCalled[0])); - Assert.That(2008040195, Is.EqualTo(_downCalled[0])); - } - - [Test] - public void MigrateDownwardWithRollback() - { - SetUpCurrentVersion(2008060195, true); - - try - { - _migrator.MigrateTo(3); - Assert.Fail("La migration 5 devrait lancer une exception"); - } - catch (Exception) - { - } - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(1, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008060195, Is.EqualTo(_downCalled[0])); - } - - [Test] - public void MigrateToCurrentVersion() - { - SetUpCurrentVersion(2008030195); - - _migrator.MigrateTo(2008030195); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - } - - [Test] - public void MigrateToLastVersion() - { - SetUpCurrentVersion(2008030195, false, false); - - _migrator.MigrateToLastVersion(); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - } - - [Test] - public void MigrateUpWithHoles() - { - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008030195); - SetUpCurrentVersion(2008030195, migs, false, false); - _migrator.MigrateTo(2008040195); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008020195, Is.EqualTo(_upCalled[0])); - Assert.That(2008040195, Is.EqualTo(_upCalled[1])); - } - - [Test] - public void MigrateUpward() - { - SetUpCurrentVersion(2008010195); - _migrator.MigrateTo(2008030195); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008020195, Is.EqualTo(_upCalled[0])); - Assert.That(2008030195, Is.EqualTo(_upCalled[1])); - } - - [Test] - public void MigrateUpwardWithRollback() - { - SetUpCurrentVersion(2008030195, true); - - try - { - _migrator.MigrateTo(2008060195); - Assert.Fail("La migration 5 devrait lancer une exception"); - } - catch (Exception) - { - } - - Assert.That(1, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008040195, Is.EqualTo(_upCalled[0])); - } - - [Test] - public void PostMergeMigrateDown() - { - // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then - // rollback to version 2. v3 should be untouched, and v4 should be rolled back - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008020195); - migs.Add(2008040195); - SetUpCurrentVersion(2008040195, migs, false, false); - _migrator.MigrateTo(2008020195); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(1, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008040195, Is.EqualTo(_downCalled[0])); - } - - [Test] - public void PostMergeOldAndMigrateLatest() - { - // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then - // we migrate to Latest. v3 should be applied and nothing else done. - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008020195); - migs.Add(2008040195); - SetUpCurrentVersion(2008040195, migs, false, false); - _migrator.MigrateTo(2008040195); - - Assert.That(1, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008030195, Is.EqualTo(_upCalled[0])); - } - - [Test] - public void ToHumanName() - { - Assert.That("Create a table", Is.EqualTo(StringUtils.ToHumanName("CreateATable"))); - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using NSubstitute; +using NUnit.Framework; + +namespace Migrator.Tests; + +[TestFixture] +public class MigratorTestDates +{ + [SetUp] + public void SetUp() + { + SetUpCurrentVersion(0); + } + + private DotNetProjects.Migrator.Migrator _migrator; + private ITransformationProvider _provider; + + // Collections that contain the version that are called migrating up and down + private static readonly List _upCalled = []; + private static readonly List _downCalled = []; + + private void SetUpCurrentVersion(long version) + { + SetUpCurrentVersion(version, false); + } + + private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled) + { + SetUpCurrentVersion(version, assertRollbackIsCalled, true); + } + + private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled, bool includeBad) + { + var appliedVersions = new List(); + + for (long i = 2008010195; i <= version; i += 10000) + { + appliedVersions.Add(i); + } + + SetUpCurrentVersion(version, appliedVersions, assertRollbackIsCalled, includeBad); + } + + private void SetUpCurrentVersion(long version, List appliedVersions, bool assertRollbackIsCalled, bool includeBad) + { + var providerMock = Substitute.For(); + + providerMock.AppliedMigrations.Returns(appliedVersions); + providerMock.Logger.Returns(new Logger(false)); + + _provider = providerMock; + + _migrator = new DotNetProjects.Migrator.Migrator((ITransformationProvider)providerMock, Assembly.GetExecutingAssembly(), false); + + _migrator.MigrationsTypes.Clear(); + _upCalled.Clear(); + _downCalled.Clear(); + + _migrator.MigrationsTypes.Add(typeof(FirstMigration)); + _migrator.MigrationsTypes.Add(typeof(SecondMigration)); + _migrator.MigrationsTypes.Add(typeof(ThirdMigration)); + _migrator.MigrationsTypes.Add(typeof(FourthMigration)); + _migrator.MigrationsTypes.Add(typeof(SixthMigration)); + + if (includeBad) + { + _migrator.MigrationsTypes.Add(typeof(BadMigration)); + } + } + + public class AbstractTestMigration : Migration + { + public override void Up() + { + _upCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); + } + + public override void Down() + { + _downCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); + } + } + + [Migration(2008010195, Ignore = true)] + public class FirstMigration : AbstractTestMigration + { + } + + [Migration(2008020195, Ignore = true)] + public class SecondMigration : AbstractTestMigration + { + } + + [Migration(2008030195, Ignore = true)] + public class ThirdMigration : AbstractTestMigration + { + } + + [Migration(2008040195, Ignore = true)] + public class FourthMigration : AbstractTestMigration + { + } + + [Migration(2008050195, Ignore = true)] + public class BadMigration : AbstractTestMigration + { + public override void Up() + { + throw new Exception("oh uh!"); + } + + public override void Down() + { + throw new Exception("oh uh!"); + } + } + + [Migration(2008060195, Ignore = true)] + public class SixthMigration : AbstractTestMigration + { + } + + [Migration(2008070195)] + public class NonIgnoredMigration : AbstractTestMigration + { + } + + [Test] + public void MigrateBackward() + { + SetUpCurrentVersion(2008030195); + _migrator.MigrateTo(2008010195); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(2, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008030195, Is.EqualTo(_downCalled[0])); + Assert.That(2008020195, Is.EqualTo(_downCalled[1])); + } + + [Test] + public void MigrateDownWithHoles() + { + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008030195); + migs.Add(2008040195); + SetUpCurrentVersion(2008040195, migs, false, false); + _migrator.MigrateTo(2008030195); + + Assert.That(1, Is.EqualTo(_upCalled.Count)); + Assert.That(1, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008020195, Is.EqualTo(_upCalled[0])); + Assert.That(2008040195, Is.EqualTo(_downCalled[0])); + } + + [Test] + public void MigrateDownwardWithRollback() + { + SetUpCurrentVersion(2008060195, true); + + Assert.That(Assert.Throws(() => _migrator.MigrateTo(3)).Message, Is.EqualTo("oh uh!")); + _provider.Received(1).Rollback(); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(1, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008060195, Is.EqualTo(_downCalled[0])); + } + + [Test] + public void MigrateToCurrentVersion() + { + SetUpCurrentVersion(2008030195); + + _migrator.MigrateTo(2008030195); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + } + + [Test] + public void MigrateToLastVersion() + { + SetUpCurrentVersion(2008030195, false, false); + + _migrator.MigrateToLastVersion(); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + } + + [Test] + public void MigrateUpWithHoles() + { + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008030195); + SetUpCurrentVersion(2008030195, migs, false, false); + _migrator.MigrateTo(2008040195); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008020195, Is.EqualTo(_upCalled[0])); + Assert.That(2008040195, Is.EqualTo(_upCalled[1])); + } + + [Test] + public void MigrateUpward() + { + SetUpCurrentVersion(2008010195); + _migrator.MigrateTo(2008030195); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008020195, Is.EqualTo(_upCalled[0])); + Assert.That(2008030195, Is.EqualTo(_upCalled[1])); + } + + [Test] + public void MigrateUpwardWithRollback() + { + SetUpCurrentVersion(2008030195, true); + + Assert.That(Assert.Throws(() => _migrator.MigrateTo(2008060195)).Message, Is.EqualTo("oh uh!")); + _provider.Received(1).Rollback(); + + Assert.That(1, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008040195, Is.EqualTo(_upCalled[0])); + } + + [Test] + public void PostMergeMigrateDown() + { + // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then + // rollback to version 2. v3 should be untouched, and v4 should be rolled back + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008020195); + migs.Add(2008040195); + SetUpCurrentVersion(2008040195, migs, false, false); + _migrator.MigrateTo(2008020195); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(1, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008040195, Is.EqualTo(_downCalled[0])); + } + + [Test] + public void PostMergeOldAndMigrateLatest() + { + // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then + // we migrate to Latest. v3 should be applied and nothing else done. + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008020195); + migs.Add(2008040195); + SetUpCurrentVersion(2008040195, migs, false, false); + _migrator.MigrateTo(2008040195); + + Assert.That(1, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008030195, Is.EqualTo(_upCalled[0])); + } + + [Test] + public void ToHumanName() + { + Assert.That("Create a table", Is.EqualTo(StringUtils.ToHumanName("CreateATable"))); + } +} From 3fa1ac336c02cb7822f19d5a6616418bc4d9ed46 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:26:35 +0200 Subject: [PATCH 5/6] Keep rollback fixture changes focused by preserving original line endings Restore LF endings in the two existing fixtures to avoid whole-file diff noise. Validation correction for 727d132: the completed Unit run had 70 passing tests, not 68; no test behavior changes in this formatting commit. --- src/Migrator.Tests/MigrationLoaderTest.cs | 138 +++--- src/Migrator.Tests/MigratorTestDates.cs | 568 +++++++++++----------- 2 files changed, 353 insertions(+), 353 deletions(-) diff --git a/src/Migrator.Tests/MigrationLoaderTest.cs b/src/Migrator.Tests/MigrationLoaderTest.cs index b7c1d641..e319dd78 100644 --- a/src/Migrator.Tests/MigrationLoaderTest.cs +++ b/src/Migrator.Tests/MigrationLoaderTest.cs @@ -1,69 +1,69 @@ -using System.Reflection; -using DotNetProjects.Migrator; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Loggers; -using NSubstitute; -using NUnit.Framework; - -namespace Migrator.Tests; - -[TestFixture] -public class MigrationLoaderTest -{ - #region Setup/Teardown - - [SetUp] - public void SetUp() - { - SetUpLoader(); - } - - #endregion - - private MigrationLoader _migrationLoader; - - private void SetUpLoader() - { - var providerMock = Substitute.For(); - - providerMock.Logger = new Logger(false); - _migrationLoader = new MigrationLoader(providerMock, Assembly.GetExecutingAssembly(), true); - _migrationLoader.MigrationsTypes.Clear(); - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.FirstMigration)); - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.SecondMigration)); - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.ThirdMigration)); - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.ForthMigration)); - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.BadMigration)); - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.SixthMigration)); - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.NonIgnoredMigration)); - } - - [Test] - public void CheckForDuplicatedVersion() - { - _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.FirstMigration)); - Assert.Throws(() => - { - _migrationLoader.CheckForDuplicatedVersion(); - }); - } - - [Test] - public void LastVersion() - { - Assert.That(7, Is.EqualTo(_migrationLoader.LastVersion)); - } - - [Test] - public void NullIfNoMigrationForVersion() - { - Assert.That(_migrationLoader.GetMigration(99999999), Is.Null); - } - - [Test] - public void ZeroIfNoMigrations() - { - _migrationLoader.MigrationsTypes.Clear(); - Assert.That(0, Is.EqualTo(_migrationLoader.LastVersion)); - } -} +using System.Reflection; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using NSubstitute; +using NUnit.Framework; + +namespace Migrator.Tests; + +[TestFixture] +public class MigrationLoaderTest +{ + #region Setup/Teardown + + [SetUp] + public void SetUp() + { + SetUpLoader(); + } + + #endregion + + private MigrationLoader _migrationLoader; + + private void SetUpLoader() + { + var providerMock = Substitute.For(); + + providerMock.Logger = new Logger(false); + _migrationLoader = new MigrationLoader(providerMock, Assembly.GetExecutingAssembly(), true); + _migrationLoader.MigrationsTypes.Clear(); + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.FirstMigration)); + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.SecondMigration)); + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.ThirdMigration)); + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.ForthMigration)); + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.BadMigration)); + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.SixthMigration)); + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.NonIgnoredMigration)); + } + + [Test] + public void CheckForDuplicatedVersion() + { + _migrationLoader.MigrationsTypes.Add(typeof(MigratorTest.FirstMigration)); + Assert.Throws(() => + { + _migrationLoader.CheckForDuplicatedVersion(); + }); + } + + [Test] + public void LastVersion() + { + Assert.That(7, Is.EqualTo(_migrationLoader.LastVersion)); + } + + [Test] + public void NullIfNoMigrationForVersion() + { + Assert.That(_migrationLoader.GetMigration(99999999), Is.Null); + } + + [Test] + public void ZeroIfNoMigrations() + { + _migrationLoader.MigrationsTypes.Clear(); + Assert.That(0, Is.EqualTo(_migrationLoader.LastVersion)); + } +} diff --git a/src/Migrator.Tests/MigratorTestDates.cs b/src/Migrator.Tests/MigratorTestDates.cs index 3660abbc..051888f8 100644 --- a/src/Migrator.Tests/MigratorTestDates.cs +++ b/src/Migrator.Tests/MigratorTestDates.cs @@ -1,284 +1,284 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using DotNetProjects.Migrator; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.Loggers; -using NSubstitute; -using NUnit.Framework; - -namespace Migrator.Tests; - -[TestFixture] -public class MigratorTestDates -{ - [SetUp] - public void SetUp() - { - SetUpCurrentVersion(0); - } - - private DotNetProjects.Migrator.Migrator _migrator; - private ITransformationProvider _provider; - - // Collections that contain the version that are called migrating up and down - private static readonly List _upCalled = []; - private static readonly List _downCalled = []; - - private void SetUpCurrentVersion(long version) - { - SetUpCurrentVersion(version, false); - } - - private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled) - { - SetUpCurrentVersion(version, assertRollbackIsCalled, true); - } - - private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled, bool includeBad) - { - var appliedVersions = new List(); - - for (long i = 2008010195; i <= version; i += 10000) - { - appliedVersions.Add(i); - } - - SetUpCurrentVersion(version, appliedVersions, assertRollbackIsCalled, includeBad); - } - - private void SetUpCurrentVersion(long version, List appliedVersions, bool assertRollbackIsCalled, bool includeBad) - { - var providerMock = Substitute.For(); - - providerMock.AppliedMigrations.Returns(appliedVersions); - providerMock.Logger.Returns(new Logger(false)); - - _provider = providerMock; - - _migrator = new DotNetProjects.Migrator.Migrator((ITransformationProvider)providerMock, Assembly.GetExecutingAssembly(), false); - - _migrator.MigrationsTypes.Clear(); - _upCalled.Clear(); - _downCalled.Clear(); - - _migrator.MigrationsTypes.Add(typeof(FirstMigration)); - _migrator.MigrationsTypes.Add(typeof(SecondMigration)); - _migrator.MigrationsTypes.Add(typeof(ThirdMigration)); - _migrator.MigrationsTypes.Add(typeof(FourthMigration)); - _migrator.MigrationsTypes.Add(typeof(SixthMigration)); - - if (includeBad) - { - _migrator.MigrationsTypes.Add(typeof(BadMigration)); - } - } - - public class AbstractTestMigration : Migration - { - public override void Up() - { - _upCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); - } - - public override void Down() - { - _downCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); - } - } - - [Migration(2008010195, Ignore = true)] - public class FirstMigration : AbstractTestMigration - { - } - - [Migration(2008020195, Ignore = true)] - public class SecondMigration : AbstractTestMigration - { - } - - [Migration(2008030195, Ignore = true)] - public class ThirdMigration : AbstractTestMigration - { - } - - [Migration(2008040195, Ignore = true)] - public class FourthMigration : AbstractTestMigration - { - } - - [Migration(2008050195, Ignore = true)] - public class BadMigration : AbstractTestMigration - { - public override void Up() - { - throw new Exception("oh uh!"); - } - - public override void Down() - { - throw new Exception("oh uh!"); - } - } - - [Migration(2008060195, Ignore = true)] - public class SixthMigration : AbstractTestMigration - { - } - - [Migration(2008070195)] - public class NonIgnoredMigration : AbstractTestMigration - { - } - - [Test] - public void MigrateBackward() - { - SetUpCurrentVersion(2008030195); - _migrator.MigrateTo(2008010195); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(2, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008030195, Is.EqualTo(_downCalled[0])); - Assert.That(2008020195, Is.EqualTo(_downCalled[1])); - } - - [Test] - public void MigrateDownWithHoles() - { - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008030195); - migs.Add(2008040195); - SetUpCurrentVersion(2008040195, migs, false, false); - _migrator.MigrateTo(2008030195); - - Assert.That(1, Is.EqualTo(_upCalled.Count)); - Assert.That(1, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008020195, Is.EqualTo(_upCalled[0])); - Assert.That(2008040195, Is.EqualTo(_downCalled[0])); - } - - [Test] - public void MigrateDownwardWithRollback() - { - SetUpCurrentVersion(2008060195, true); - - Assert.That(Assert.Throws(() => _migrator.MigrateTo(3)).Message, Is.EqualTo("oh uh!")); - _provider.Received(1).Rollback(); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(1, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008060195, Is.EqualTo(_downCalled[0])); - } - - [Test] - public void MigrateToCurrentVersion() - { - SetUpCurrentVersion(2008030195); - - _migrator.MigrateTo(2008030195); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - } - - [Test] - public void MigrateToLastVersion() - { - SetUpCurrentVersion(2008030195, false, false); - - _migrator.MigrateToLastVersion(); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - } - - [Test] - public void MigrateUpWithHoles() - { - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008030195); - SetUpCurrentVersion(2008030195, migs, false, false); - _migrator.MigrateTo(2008040195); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008020195, Is.EqualTo(_upCalled[0])); - Assert.That(2008040195, Is.EqualTo(_upCalled[1])); - } - - [Test] - public void MigrateUpward() - { - SetUpCurrentVersion(2008010195); - _migrator.MigrateTo(2008030195); - - Assert.That(2, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008020195, Is.EqualTo(_upCalled[0])); - Assert.That(2008030195, Is.EqualTo(_upCalled[1])); - } - - [Test] - public void MigrateUpwardWithRollback() - { - SetUpCurrentVersion(2008030195, true); - - Assert.That(Assert.Throws(() => _migrator.MigrateTo(2008060195)).Message, Is.EqualTo("oh uh!")); - _provider.Received(1).Rollback(); - - Assert.That(1, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008040195, Is.EqualTo(_upCalled[0])); - } - - [Test] - public void PostMergeMigrateDown() - { - // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then - // rollback to version 2. v3 should be untouched, and v4 should be rolled back - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008020195); - migs.Add(2008040195); - SetUpCurrentVersion(2008040195, migs, false, false); - _migrator.MigrateTo(2008020195); - - Assert.That(0, Is.EqualTo(_upCalled.Count)); - Assert.That(1, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008040195, Is.EqualTo(_downCalled[0])); - } - - [Test] - public void PostMergeOldAndMigrateLatest() - { - // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then - // we migrate to Latest. v3 should be applied and nothing else done. - var migs = new List(); - migs.Add(2008010195); - migs.Add(2008020195); - migs.Add(2008040195); - SetUpCurrentVersion(2008040195, migs, false, false); - _migrator.MigrateTo(2008040195); - - Assert.That(1, Is.EqualTo(_upCalled.Count)); - Assert.That(0, Is.EqualTo(_downCalled.Count)); - - Assert.That(2008030195, Is.EqualTo(_upCalled[0])); - } - - [Test] - public void ToHumanName() - { - Assert.That("Create a table", Is.EqualTo(StringUtils.ToHumanName("CreateATable"))); - } -} +using System; +using System.Collections.Generic; +using System.Reflection; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using NSubstitute; +using NUnit.Framework; + +namespace Migrator.Tests; + +[TestFixture] +public class MigratorTestDates +{ + [SetUp] + public void SetUp() + { + SetUpCurrentVersion(0); + } + + private DotNetProjects.Migrator.Migrator _migrator; + private ITransformationProvider _provider; + + // Collections that contain the version that are called migrating up and down + private static readonly List _upCalled = []; + private static readonly List _downCalled = []; + + private void SetUpCurrentVersion(long version) + { + SetUpCurrentVersion(version, false); + } + + private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled) + { + SetUpCurrentVersion(version, assertRollbackIsCalled, true); + } + + private void SetUpCurrentVersion(long version, bool assertRollbackIsCalled, bool includeBad) + { + var appliedVersions = new List(); + + for (long i = 2008010195; i <= version; i += 10000) + { + appliedVersions.Add(i); + } + + SetUpCurrentVersion(version, appliedVersions, assertRollbackIsCalled, includeBad); + } + + private void SetUpCurrentVersion(long version, List appliedVersions, bool assertRollbackIsCalled, bool includeBad) + { + var providerMock = Substitute.For(); + + providerMock.AppliedMigrations.Returns(appliedVersions); + providerMock.Logger.Returns(new Logger(false)); + + _provider = providerMock; + + _migrator = new DotNetProjects.Migrator.Migrator((ITransformationProvider)providerMock, Assembly.GetExecutingAssembly(), false); + + _migrator.MigrationsTypes.Clear(); + _upCalled.Clear(); + _downCalled.Clear(); + + _migrator.MigrationsTypes.Add(typeof(FirstMigration)); + _migrator.MigrationsTypes.Add(typeof(SecondMigration)); + _migrator.MigrationsTypes.Add(typeof(ThirdMigration)); + _migrator.MigrationsTypes.Add(typeof(FourthMigration)); + _migrator.MigrationsTypes.Add(typeof(SixthMigration)); + + if (includeBad) + { + _migrator.MigrationsTypes.Add(typeof(BadMigration)); + } + } + + public class AbstractTestMigration : Migration + { + public override void Up() + { + _upCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); + } + + public override void Down() + { + _downCalled.Add(MigrationLoader.GetMigrationVersion(GetType())); + } + } + + [Migration(2008010195, Ignore = true)] + public class FirstMigration : AbstractTestMigration + { + } + + [Migration(2008020195, Ignore = true)] + public class SecondMigration : AbstractTestMigration + { + } + + [Migration(2008030195, Ignore = true)] + public class ThirdMigration : AbstractTestMigration + { + } + + [Migration(2008040195, Ignore = true)] + public class FourthMigration : AbstractTestMigration + { + } + + [Migration(2008050195, Ignore = true)] + public class BadMigration : AbstractTestMigration + { + public override void Up() + { + throw new Exception("oh uh!"); + } + + public override void Down() + { + throw new Exception("oh uh!"); + } + } + + [Migration(2008060195, Ignore = true)] + public class SixthMigration : AbstractTestMigration + { + } + + [Migration(2008070195)] + public class NonIgnoredMigration : AbstractTestMigration + { + } + + [Test] + public void MigrateBackward() + { + SetUpCurrentVersion(2008030195); + _migrator.MigrateTo(2008010195); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(2, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008030195, Is.EqualTo(_downCalled[0])); + Assert.That(2008020195, Is.EqualTo(_downCalled[1])); + } + + [Test] + public void MigrateDownWithHoles() + { + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008030195); + migs.Add(2008040195); + SetUpCurrentVersion(2008040195, migs, false, false); + _migrator.MigrateTo(2008030195); + + Assert.That(1, Is.EqualTo(_upCalled.Count)); + Assert.That(1, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008020195, Is.EqualTo(_upCalled[0])); + Assert.That(2008040195, Is.EqualTo(_downCalled[0])); + } + + [Test] + public void MigrateDownwardWithRollback() + { + SetUpCurrentVersion(2008060195, true); + + Assert.That(Assert.Throws(() => _migrator.MigrateTo(3)).Message, Is.EqualTo("oh uh!")); + _provider.Received(1).Rollback(); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(1, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008060195, Is.EqualTo(_downCalled[0])); + } + + [Test] + public void MigrateToCurrentVersion() + { + SetUpCurrentVersion(2008030195); + + _migrator.MigrateTo(2008030195); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + } + + [Test] + public void MigrateToLastVersion() + { + SetUpCurrentVersion(2008030195, false, false); + + _migrator.MigrateToLastVersion(); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + } + + [Test] + public void MigrateUpWithHoles() + { + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008030195); + SetUpCurrentVersion(2008030195, migs, false, false); + _migrator.MigrateTo(2008040195); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008020195, Is.EqualTo(_upCalled[0])); + Assert.That(2008040195, Is.EqualTo(_upCalled[1])); + } + + [Test] + public void MigrateUpward() + { + SetUpCurrentVersion(2008010195); + _migrator.MigrateTo(2008030195); + + Assert.That(2, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008020195, Is.EqualTo(_upCalled[0])); + Assert.That(2008030195, Is.EqualTo(_upCalled[1])); + } + + [Test] + public void MigrateUpwardWithRollback() + { + SetUpCurrentVersion(2008030195, true); + + Assert.That(Assert.Throws(() => _migrator.MigrateTo(2008060195)).Message, Is.EqualTo("oh uh!")); + _provider.Received(1).Rollback(); + + Assert.That(1, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008040195, Is.EqualTo(_upCalled[0])); + } + + [Test] + public void PostMergeMigrateDown() + { + // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then + // rollback to version 2. v3 should be untouched, and v4 should be rolled back + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008020195); + migs.Add(2008040195); + SetUpCurrentVersion(2008040195, migs, false, false); + _migrator.MigrateTo(2008020195); + + Assert.That(0, Is.EqualTo(_upCalled.Count)); + Assert.That(1, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008040195, Is.EqualTo(_downCalled[0])); + } + + [Test] + public void PostMergeOldAndMigrateLatest() + { + // Assume trunk had versions 1 2 and 4. A branch is merged with 3, then + // we migrate to Latest. v3 should be applied and nothing else done. + var migs = new List(); + migs.Add(2008010195); + migs.Add(2008020195); + migs.Add(2008040195); + SetUpCurrentVersion(2008040195, migs, false, false); + _migrator.MigrateTo(2008040195); + + Assert.That(1, Is.EqualTo(_upCalled.Count)); + Assert.That(0, Is.EqualTo(_downCalled.Count)); + + Assert.That(2008030195, Is.EqualTo(_upCalled[0])); + } + + [Test] + public void ToHumanName() + { + Assert.That("Create a table", Is.EqualTo(StringUtils.ToHumanName("CreateATable"))); + } +} From c8619f99bf0dfd912c0005fc0cd3e5de0599dabc Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:32:23 +0200 Subject: [PATCH 6/6] Close legacy dry-run, empty-run and callback-context safety gaps Defer legacy runner history access until navigation and use IMigrationHistory for read-only reads; constructors and DryRun no longer initialize history. Empty latest-version runs return before history access. Restore migration context around post-commit callbacks and clear it even when a callback fails. Update README scope semantics to match the explicitly requested effective-scope behavior. Validation: solution build, Unit 70 passed, SQLite 145 passed with one pre-existing default-removal skip handled in the provider layer. New regressions cover legacy navigation, empty assemblies, callback context, durable history and callback failure cleanup. Addresses reviews 4071830715, 4071830792 and 4071830843; documents the deliberate scope policy discussed in 4071742900. --- README.md | 6 ++-- src/Migrator.Tests/RunnerSafetyTests.cs | 41 +++++++++++++++++++++++++ src/Migrator/BaseMigrate.cs | 32 ++++++++++++++----- src/Migrator/MigrateAnywhere.cs | 6 +--- src/Migrator/MigrationExecution.cs | 11 ++++++- src/Migrator/Migrator.cs | 5 +++ 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 285a7ee0..e8f8b420 100644 --- a/README.md +++ b/README.md @@ -180,9 +180,9 @@ billingMigrator.MigrateToLastVersion(); Important details: -- A scope partitions **history**, not migration discovery. Passing a mixed assembly does not automatically filter it by scope. -- Leave `MigrationAttribute.Scope` unset to inherit the provider scope. If you override it, it must align with the history the runner reads. -- Duplicate versions are checked across the runner's whole loaded migration set. Separate scopes do not permit duplicate versions within one runner. +- In the upgrade source, explicit scopes filter discovery; unscoped migrations inherit the runner scope. A scope partitions history, not database objects. +- Leave `MigrationAttribute.Scope` unset to inherit the provider scope; set it to select a migration for one specific scope. +- Duplicate versions are checked within the effective scope. Duplicate versions in distinct explicit scopes are independent. - Scopes do not isolate tables or data. Module migrations still need compatible table names and coordinated schema ownership. See [ProviderFactory](src/Migrator/ProviderFactory.cs), [MigrationLoader](src/Migrator/MigrationLoader.cs) and [history implementation](src/Migrator/Providers/TransformationProvider.cs). diff --git a/src/Migrator.Tests/RunnerSafetyTests.cs b/src/Migrator.Tests/RunnerSafetyTests.cs index e5fe5898..b95a6c05 100644 --- a/src/Migrator.Tests/RunnerSafetyTests.cs +++ b/src/Migrator.Tests/RunnerSafetyTests.cs @@ -24,6 +24,47 @@ public class RunnerSafetyTests public override void Up() => throw new InvalidOperationException("original"); public override void Down() => throw new InvalidOperationException("original"); } + [Migration(4, Ignore = true)] public class CallbackContext : One + { + public override void AfterUp() + { + if (!ReferenceEquals(((TransformationProvider)Database).CurrentMigration, this)) throw new InvalidOperationException("Missing callback context."); + Database.ExecuteNonQuery("INSERT INTO Example VALUES (1)"); + } + } + [Migration(5, Ignore = true)] public class FailedCallbackContext : CallbackContext + { public override void AfterUp() { base.AfterUp(); throw new InvalidOperationException("Callback failed."); } } + + [Test, Category("SQLite")] public void LegacyDryRunConstructionAndNavigationAreReadOnly() + { + using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True"); connection.Open(); + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); + var legacy = new MigrateAnywhere(new List { 1 }, provider, new Logger(false)) { DryRun = true }; + Assert.That(legacy.Current, Is.Zero); + Assert.That(legacy.AppliedVersions, Is.Empty); + Assert.That(legacy.Continue(1), Is.True); + legacy.Migrate(new One { Database = provider }); + Assert.That(provider.TableExists("SchemaInfo"), Is.False); + Assert.That(provider.TableExists("Example"), Is.False); + Assert.That(((DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteTransformationProvider)provider).IsPragmaForeignKeysOn(), Is.True); + } + [Test, Category("SQLite")] public void EmptyLatestRunDoesNotCreateHistory() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + new DotNetProjects.Migrator.Migrator(provider, false, Array.Empty()).MigrateToLastVersion(); + Assert.That(provider.TableExists("SchemaInfo"), Is.False); + } + [TestCase(false), TestCase(true), Category("SQLite")] + public void PostCommitCallbackHasContextAndAlwaysClearsIt(bool fail) + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + var runner = new DotNetProjects.Migrator.Migrator(provider, false, fail ? typeof(FailedCallbackContext) : typeof(CallbackContext)); + if (fail) Assert.That(Assert.Throws(runner.MigrateToLastVersion).Message, Is.EqualTo("Callback failed.")); + else runner.MigrateToLastVersion(); + Assert.That(((TransformationProvider)provider).CurrentMigration, Is.Null); + Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT COUNT(*) FROM Example")), Is.EqualTo(1)); + Assert.That(provider.AppliedMigrations, Has.Count.EqualTo(1)); + } [Test] public void CustomProvidersRetainExplicitMigrationScope() { var provider = Substitute.For(); diff --git a/src/Migrator/BaseMigrate.cs b/src/Migrator/BaseMigrate.cs index a8d9ea69..dd6f5a72 100644 --- a/src/Migrator/BaseMigrate.cs +++ b/src/Migrator/BaseMigrate.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using System.Linq; using DotNetProjects.Migrator.Framework; namespace DotNetProjects.Migrator; @@ -11,24 +13,39 @@ public abstract class BaseMigrate protected bool _dryrun; protected ILogger _logger; protected List _original; + private bool _initialized; protected BaseMigrate(List availableMigrations, ITransformationProvider provider, ILogger logger) { _provider = provider; - _availableMigrations = availableMigrations; - _original = new List(_provider.AppliedMigrations.ToArray()); //clone + _availableMigrations = availableMigrations.OrderBy(version => version).ToList(); _logger = logger; } + protected IReadOnlyList ReadHistory() + { + if (_provider is IMigrationHistory history) return history.ReadAppliedMigrations(); + if (DryRun) throw new NotSupportedException("Legacy dry-run requires IMigrationHistory on custom providers."); + return _provider.AppliedMigrations; + } + + private void InitializeHistory() + { + if (_initialized) return; + _original = new List(ReadHistory()); + _current = _original.DefaultIfEmpty(0).Max(); + _initialized = true; + } + public List AppliedVersions { - get { return _original; } + get { InitializeHistory(); return _original; } } public virtual long Current { - get { return _current; } - protected set { _current = value; } + get { InitializeHistory(); return _current; } + protected set { InitializeHistory(); _current = value; } } public virtual bool DryRun @@ -61,12 +78,13 @@ public void Iterate() /// The migration number of the next available Migration. protected long NextMigration() { + if (_availableMigrations.Count == 0) return 0; // Start searching at the current index var migrationSearch = _availableMigrations.IndexOf(Current) + 1; // See if we can find a migration that matches the requirement while (migrationSearch < _availableMigrations.Count - && _provider.AppliedMigrations.Contains(_availableMigrations[migrationSearch])) + && ReadHistory().Contains(_availableMigrations[migrationSearch])) { migrationSearch++; } @@ -93,7 +111,7 @@ protected long PreviousMigration() // See if we can find a migration that matches the requirement while (migrationSearch > -1 - && !_provider.AppliedMigrations.Contains(_availableMigrations[migrationSearch])) + && !ReadHistory().Contains(_availableMigrations[migrationSearch])) { migrationSearch--; } diff --git a/src/Migrator/MigrateAnywhere.cs b/src/Migrator/MigrateAnywhere.cs index ae096a37..8813ba97 100644 --- a/src/Migrator/MigrateAnywhere.cs +++ b/src/Migrator/MigrateAnywhere.cs @@ -17,11 +17,6 @@ public class MigrateAnywhere : BaseMigrate public MigrateAnywhere(List availableMigrations, ITransformationProvider provider, ILogger logger) : base(availableMigrations, provider, logger) { - _current = 0; - if (provider.AppliedMigrations.Count > 0) - { - _current = provider.AppliedMigrations.Max(); - } _goForward = false; } @@ -47,6 +42,7 @@ public override long Previous public override bool Continue(long version) { + if (_availableMigrations.Count == 0) return false; // If we're going backwards and our current is less than the target, // reverse direction. Also, start over at zero to make sure we catch // any merged migrations that are less than the current target. diff --git a/src/Migrator/MigrationExecution.cs b/src/Migrator/MigrationExecution.cs index ef5a3421..eb7c148b 100644 --- a/src/Migrator/MigrationExecution.cs +++ b/src/Migrator/MigrationExecution.cs @@ -53,6 +53,15 @@ internal static void Execute(ITransformationProvider provider, IMigration migrat } } // These callbacks intentionally run after commit; failure cannot be rolled back. - if (step.IsUp) migration.AfterUp(); else migration.AfterDown(); + After(provider, migration, step.IsUp); } + internal static void After(ITransformationProvider provider, IMigration migration, bool up) + { + var concrete = provider as TransformationProvider; + var previous = concrete?.CurrentMigration; + if (concrete != null) concrete.CurrentMigration = migration; + try { if (up) migration.AfterUp(); else migration.AfterDown(); } + finally { if (concrete != null) concrete.CurrentMigration = previous; } + } + } diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index cd16c046..f900dd5c 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -182,6 +182,11 @@ public long? LastAppliedMigrationVersion /// public void MigrateToLastVersion() { + if (_migrationLoader.GetAvailableMigrations().Count == 0) + { + Logger.Warn("No migrations found for the effective scope."); + return; + } MigrateTo(_migrationLoader.LastVersion); }