From 281628047df688a5d46c223a2076f12bc89ef2f3 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 14:52:28 +0200 Subject: [PATCH 01/17] Add runner filtering, lifecycle stages and transaction modes Introduce additive runner options for tag any/all matching, ordered unversioned profiles, maintenance stages, activation and lock leases. Acquire locks before history refresh and keep version history isolated from auxiliary migrations. Preserve per-migration defaults; add no-transaction and verified-provider whole-session execution with callbacks deferred until commit. Validation: rebuilt solution; SQLite 156 tests and Unit 73 tests passed. Behavioral coverage checks rollback boundaries, post-commit ordering, profile history, tag matching and lock release. Native lock implementations and tooling follow separately. --- src/Migrator.Tests/RunnerFeatureTests.cs | 110 ++++++++ src/Migrator/MigrationExecution.cs | 64 +++-- src/Migrator/MigrationLoader.cs | 341 +++++++++++------------ src/Migrator/Migrator.cs | 95 +++++-- src/Migrator/RunnerOptions.cs | 35 +++ 5 files changed, 414 insertions(+), 231 deletions(-) create mode 100644 src/Migrator.Tests/RunnerFeatureTests.cs create mode 100644 src/Migrator/RunnerOptions.cs diff --git a/src/Migrator.Tests/RunnerFeatureTests.cs b/src/Migrator.Tests/RunnerFeatureTests.cs new file mode 100644 index 00000000..ddd3fb30 --- /dev/null +++ b/src/Migrator.Tests/RunnerFeatureTests.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Data; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers; +using Microsoft.Data.Sqlite; +using NUnit.Framework; +namespace Migrator.Tests; + +[Category("SQLite")] +public class RunnerFeatureTests +{ + private static readonly List Events = new(); + [Migration(1), Tags("blue", "shared")] + internal class First : Migration + { + public override void Up() { Events.Add("first"); Database.AddTable("First", new Column("Id", DbType.Int32)); } + public override void Down() => Database.RemoveTable("First"); + public override void AfterUp() => Events.Add("committed"); + } + [Migration(2), Tags("red", "shared")] + internal class Second : Migration + { + public override void Up() { Events.Add("second"); Database.AddTable("Second", new Column("Id", DbType.Int32)); } + public override void Down() => Database.RemoveTable("Second"); + } + [Migration(3)] internal class Failure : Migration + { + public override void Up() => throw new InvalidOperationException("migration failed"); + public override void Down() => throw new NotSupportedException(); + } + [Profile("seed")] internal class Seed : Migration + { + public override void Up() { Events.Add("profile"); Database.Insert("First", new[] { "Id" }, new object[] { 7 }); } + public override void Down() => throw new NotSupportedException(); + } + [Maintenance(MaintenanceStage.BeforeRun)] internal class Before : Migration + { + public override void Up() => Events.Add("before"); + public override void Down() => throw new NotSupportedException(); + } + [Maintenance(MaintenanceStage.AfterRun)] internal class After : Migration + { + public override void Up() => Events.Add("after"); + public override void Down() => throw new NotSupportedException(); + } + [SetUp] public void Reset() => Events.Clear(); + private static ITransformationProvider Provider() + { + // Provider owns this connection, so disposal also closes the in-memory database. + return ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + } + [Test] public void ProfilesAndMaintenanceHaveDeterministicOrderAndNoHistory() + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(After), typeof(Seed), typeof(First), typeof(Before)); + runner.Options.Profiles.Add("seed"); + runner.MigrateToLastVersion(); + Assert.That(Events, Is.EqualTo(new[] { "before", "first", "committed", "profile", "after" })); + Assert.That(p.AppliedMigrations, Is.EqualTo(new long[] { 1 })); + Assert.That(Convert.ToInt64(p.ExecuteScalar("SELECT Id FROM First")), Is.EqualTo(7)); + } + [TestCase(TagMatchMode.Any, 2)] + [TestCase(TagMatchMode.All, 1)] + public void TagsUseExplicitAnyOrAll(TagMatchMode mode, int expected) + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(Second), typeof(First)); + runner.Options.TagMatch = mode; + runner.Options.Tags.Add("blue"); runner.Options.Tags.Add("shared"); + runner.MigrateTo(2); + Assert.That(p.AppliedMigrations.Count, Is.EqualTo(expected)); + } + [TestCase(MigrationTransactionMode.WholeSession, false)] + [TestCase(MigrationTransactionMode.PerMigration, true)] + [TestCase(MigrationTransactionMode.None, true)] + public void TransactionModeDefinesFailureBoundary(MigrationTransactionMode mode, bool firstRemains) + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(First), typeof(Failure)); + runner.Options.TransactionMode = mode; + Assert.Throws(() => runner.MigrateToLastVersion()); + Assert.That(p.TableExists("First"), Is.EqualTo(firstRemains)); + Assert.That(p.AppliedMigrations.Contains(1), Is.EqualTo(firstRemains)); + Assert.That(Events.Contains("committed"), Is.EqualTo(firstRemains)); + } + [Test] public void SessionCallbacksRunAfterAllMigrationsAndCommit() + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(First), typeof(Second)); + runner.Options.TransactionMode = MigrationTransactionMode.WholeSession; + runner.MigrateToLastVersion(); + Assert.That(Events, Is.EqualTo(new[] { "first", "second", "committed" })); + } + [Test] public void LockPrecedesHistoryAndReleasesOnFailure() + { + using var p = Provider(); var migrationLock = new ProbeLock(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(Failure)); runner.Options.Lock = migrationLock; + Assert.Throws(() => runner.MigrateToLastVersion()); + Assert.That(migrationLock.Disposed, Is.True); + } + private sealed class ProbeLock : IMigrationLock, IDisposable + { + public bool Disposed { get; private set; } + public IDisposable Acquire(ITransformationProvider p, string scope, TimeSpan timeout) + { Assert.That(p.TableExists(p.SchemaInfoTable), Is.False); return this; } + public void Dispose() => Disposed = true; + } +} diff --git a/src/Migrator/MigrationExecution.cs b/src/Migrator/MigrationExecution.cs index eb7c148b..58e6b1eb 100644 --- a/src/Migrator/MigrationExecution.cs +++ b/src/Migrator/MigrationExecution.cs @@ -7,29 +7,51 @@ namespace DotNetProjects.Migrator; internal static class MigrationExecution { - internal static void Execute(ITransformationProvider provider, IMigration migration, MigrationStep step, ILogger logger) + internal static void Execute(ITransformationProvider provider, IMigration migration, MigrationStep step, ILogger logger, + bool transaction = true, bool inSession = false, bool recordHistory = true, bool callbacks = true) { var concrete = provider as TransformationProvider; - if (concrete?.HasActiveTransaction == true) + try + { + void Body() + { + 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 (provider is SQLiteTransformationProvider sqlite && !sqlite.CheckForeignKeyIntegrity()) + throw new MigrationException("Migration would leave invalid SQLite foreign keys."); + if (recordHistory) + { + var scope = migration.GetType().GetCustomAttribute()?.Scope ?? (provider as IMigrationHistory)?.Scope; + if (step.IsUp) provider.MigrationApplied(step.Version, scope); + else provider.MigrationUnApplied(step.Version, scope); + } + } + if (inSession) Body(); else InTransaction(provider, transaction, Body); + } + catch (Exception ex) { logger.Exception(step.Version, migration.Name, ex); throw; } + finally { if (concrete != null) concrete.CurrentMigration = null; } + // Session callbacks are deferred until the outer transaction commits. + if (callbacks) After(migration, step.IsUp); + } + + internal static void After(IMigration migration, bool up) + { if (up) migration.AfterUp(); else migration.AfterDown(); } + + internal static void InTransaction(ITransformationProvider provider, bool transaction, Action body) + { + if ((provider as TransformationProvider)?.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; + var foreignKeys = transaction && 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, 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; + if (transaction) { provider.BeginTransaction(); began = true; } + body(); + if (transaction) { provider.Commit(); began = false; } } catch (Exception ex) { @@ -39,29 +61,17 @@ internal static void Execute(ITransformationProvider provider, IMigration migrat 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; } + (provider as IMigrationHistory)?.InvalidateHistory(); } - // These callbacks intentionally run after commit; failure cannot be rolled back. - 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/MigrationLoader.cs b/src/Migrator/MigrationLoader.cs index 4be11204..70a220e7 100644 --- a/src/Migrator/MigrationLoader.cs +++ b/src/Migrator/MigrationLoader.cs @@ -1,175 +1,166 @@ -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 => - _provider is not IMigrationHistory history || - t.GetCustomAttribute()?.Scope is not string scope || scope == history.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); - } -} +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}", (t.GetCustomAttribute()?.Version.ToString() ?? "aux").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}", (t.GetCustomAttribute()?.Version.ToString() ?? "aux").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 Func Activator { get; set; } + + public IEnumerable SelectedTypes => _migrationsTypes.Where(t => + t.GetCustomAttribute() != null && InScope(t.GetCustomAttribute().Scope)); + + internal bool InScope(string scope) => scope == null || _provider is not IMigrationHistory history || scope == history.Scope; + internal IEnumerable AuxiliaryTypes => _migrationsTypes.Where(t => t.GetCustomAttribute() == null); + + + 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 (t.IsAbstract || !typeof(IMigration).IsAssignableFrom(t)) continue; + var versioned = t.GetCustomAttribute(); + if (versioned != null ? !versioned.Ignore : + t.GetCustomAttribute() != null || t.GetCustomAttribute() != null) + migrations.Add(t); + } + migrations = migrations.OrderBy(t => t.GetCustomAttribute()?.Version ?? 0).ThenBy(t => t.FullName, StringComparer.Ordinal).ToList(); + 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() + { + return SelectedTypes.Select(GetMigrationVersion).OrderBy(v => v).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 Activator != null ? Activator(migrationType) ?? throw new MigrationException("Migration activator returned null.") : (IMigration)System.Activator.CreateInstance(migrationType); + } +} diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index f900dd5c..1a2fe0fb 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -26,6 +26,7 @@ namespace DotNetProjects.Migrator; /// public class Migrator { + public RunnerOptions Options { get; } = new(); private readonly MigrationLoader _migrationLoader; private readonly ITransformationProvider _provider; @@ -182,12 +183,7 @@ public long? LastAppliedMigrationVersion /// public void MigrateToLastVersion() { - if (_migrationLoader.GetAvailableMigrations().Count == 0) - { - Logger.Warn("No migrations found for the effective scope."); - return; - } - MigrateTo(_migrationLoader.LastVersion); + MigrateTo(SelectedMigrationTypes.Select(MigrationLoader.GetMigrationVersion).DefaultIfEmpty(0).Max()); } /// @@ -201,43 +197,84 @@ public void MigrateToLastVersion() /// If dryrun is set, don't write any changes to the database. /// /// The version that must became the current one - public IReadOnlyList Plan(long version) + private IEnumerable SelectedMigrationTypes => _migrationLoader.SelectedTypes.Where(t => + { + if (Options.Tags.Count == 0) return true; + var tags = t.GetCustomAttribute()?.Tags ?? Array.Empty(); + return Options.TagMatch == TagMatchMode.All ? Options.Tags.All(tags.Contains) : Options.Tags.Any(tags.Contains); + }); + + private IReadOnlyList CreatePlan(IEnumerable applied, long version) { _migrationLoader.CheckForDuplicatedVersion(); + var selected = SelectedMigrationTypes.Select(MigrationLoader.GetMigrationVersion).ToHashSet(); + var known = _migrationLoader.GetAvailableMigrations().ToHashSet(); + // Filtered migrations stay applied; unknown history must still fail a downgrade. + return MigrationPlanner.Create(selected, applied.Where(v => selected.Contains(v) || !known.Contains(v)), version); + } + + public IReadOnlyList Plan(long version) + { 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); + return CreatePlan(history.ReadAppliedMigrations(), version); } public void MigrateTo(long version) { - _migrationLoader.CheckForDuplicatedVersion(); - 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); + if (DryRun) + { + foreach (var step in Plan(version)) + if (step.IsUp) Logger.MigrateUp(step.Version, "Preview"); else Logger.MigrateDown(step.Version, "Preview"); + return; + } + if (Options.LockTimeout < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(Options.LockTimeout)); + var session = Options.TransactionMode == MigrationTransactionMode.WholeSession; + if (session && _provider.Dialect is not (Providers.Impl.SQLite.SQLiteDialect or Providers.Impl.PostgreSQL.PostgreSQLDialect or Providers.Impl.SqlServer.SqlServerDialect)) + throw new NotSupportedException("Whole-session transactions require a verified transactional DDL provider (SQLite, PostgreSQL or SQL Server)."); + _migrationLoader.Activator = Options.Activator; + using var lease = Options.Lock?.Acquire(_provider, (_provider as IMigrationHistory)?.Scope, Options.LockTimeout); + (_provider as IMigrationHistory)?.InvalidateHistory(); + var history = new List(_provider.AppliedMigrations); + var plan = CreatePlan(history, version); + var profiles = _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } p && Options.Profiles.Contains(p.Name) && _migrationLoader.InScope(p.Scope)) + .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal).ToArray(); + foreach (var name in Options.Profiles) + if (!profiles.Any(t => t.GetCustomAttribute().Name == name)) throw new MigrationException("Unknown profile: " + name); + var afterCommit = new List(); var firstRun = true; - foreach (var step in plan) + void Execute(IMigration migration, MigrationStep step, bool record) { - if (DryRun) - { - if (step.IsUp) Logger.MigrateUp(step.Version, "Preview"); - else Logger.MigrateDown(step.Version, "Preview"); - continue; - } - var migration = _migrationLoader.GetMigration(step.Version); - if (firstRun) + migration.Database = _provider; + if (firstRun) { migration.InitializeOnce(_args); firstRun = false; } + MigrationExecution.Execute(_provider, migration, step, Logger, + Options.TransactionMode == MigrationTransactionMode.PerMigration, session, record, !session); + if (session) afterCommit.Add(() => MigrationExecution.After(migration, step.IsUp)); + } + void Maintenance(MaintenanceStage stage) + { + foreach (var type in _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } a && a.Stage == stage && _migrationLoader.InScope(a.Scope)) + .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal)) + Execute(_migrationLoader.CreateInstance(type), new MigrationStep(0, true), false); + } + void Run() + { + Maintenance(MaintenanceStage.BeforeRun); + foreach (var step in plan) { - migration.InitializeOnce(_args); - firstRun = false; + Maintenance(MaintenanceStage.BeforeMigration); + Execute(_migrationLoader.GetMigration(step.Version), step, true); + if (step.IsUp) history.Add(step.Version); else history.Remove(step.Version); + Maintenance(MaintenanceStage.AfterMigration); } - MigrationExecution.Execute(_provider, migration, step, Logger); - if (step.IsUp) history.Add(step.Version); - else history.Remove(step.Version); + foreach (var type in profiles) Execute(_migrationLoader.CreateInstance(type), new MigrationStep(0, true), false); + Maintenance(MaintenanceStage.AfterRun); } + Logger.Started(history, version); + if (session) MigrationExecution.InTransaction(_provider, true, Run); else Run(); + foreach (var callback in afterCommit) callback(); history.Sort(); Logger.Finished(history, version); } } + diff --git a/src/Migrator/RunnerOptions.cs b/src/Migrator/RunnerOptions.cs new file mode 100644 index 00000000..b473ea5a --- /dev/null +++ b/src/Migrator/RunnerOptions.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using DotNetProjects.Migrator.Framework; +namespace DotNetProjects.Migrator; + +public enum TagMatchMode { Any, All } +public enum MigrationTransactionMode { PerMigration, None, WholeSession } +public enum MaintenanceStage { BeforeRun, BeforeMigration, AfterMigration, AfterRun } + +[AttributeUsage(AttributeTargets.Class, Inherited = true)] +public sealed class TagsAttribute(params string[] tags) : Attribute +{ public IReadOnlyList Tags { get; } = Array.AsReadOnly((string[])tags.Clone()); } +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class ProfileAttribute(string name) : Attribute +{ public string Name { get; } = name; public int Order { get; set; } public string Scope { get; set; } } +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class MaintenanceAttribute(MaintenanceStage stage) : Attribute +{ public MaintenanceStage Stage { get; } = stage; public int Order { get; set; } public string Scope { get; set; } } + +public sealed class RunnerOptions +{ + public ISet Tags { get; } = new HashSet(StringComparer.Ordinal); + public TagMatchMode TagMatch { get; set; } = TagMatchMode.Any; + public ISet Profiles { get; } = new HashSet(StringComparer.Ordinal); + public MigrationTransactionMode TransactionMode { get; set; } = MigrationTransactionMode.PerMigration; + public Func Activator { get; set; } + public IMigrationLock Lock { get; set; } + public TimeSpan LockTimeout { get; set; } = TimeSpan.FromSeconds(30); +} + +/// Acquire before any history read. The lease must release its lock in Dispose. +public interface IMigrationLock +{ + IDisposable Acquire(ITransformationProvider provider, string scope, TimeSpan timeout); +} From 55b5811b4486a9f3efc33b85d5acb639b82ebd22 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:01:18 +0200 Subject: [PATCH 02/17] Add guarded SQL preview, native locks, CLI and DI integration Expose offline and connected SQL generation with opt-in legacy migration capture and explicit rejection of connection access. Add session-owned SQL Server, PostgreSQL and MySQL/MariaDB locks with timeout and release leases. Package a .NET tool for list, status, validation, migrate, rollback, plan and SQL output. Keep Microsoft dependency injection/options/logging dependencies in an optional package and omit sensitive exception/SQL details in tooling output. Validation: solution rebuilt; Unit 77 and SQLite 159 passed. Added DI activation and CLI argument/list checks, legacy preview opt-in/read-only checks, and live independent-session locking tests for the database CI matrix. --- Migrator.slnx | 8 ++ ...ator.Extensions.DependencyInjection.csproj | 9 ++ .../MigrationLogger.cs | 22 ++++ .../ServiceCollectionExtensions.cs | 31 +++++ src/Migrator.Tests/DatabaseLockTests.cs | 44 +++++++ src/Migrator.Tests/Migrator.Tests.csproj | 2 + src/Migrator.Tests/RunnerFeatureTests.cs | 26 ++++ src/Migrator.Tests/ToolingTests.cs | 49 ++++++++ .../DotNetProjects.Migrator.Tool.csproj | 12 ++ src/Migrator.Tool/Program.cs | 115 ++++++++++++++++++ src/Migrator/DatabaseMigrationLock.cs | 76 ++++++++++++ src/Migrator/MigrationSqlPreview.cs | 62 ++++++++++ src/Migrator/Migrator.cs | 31 ++++- 13 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj create mode 100644 src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs create mode 100644 src/Migrator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/Migrator.Tests/DatabaseLockTests.cs create mode 100644 src/Migrator.Tests/ToolingTests.cs create mode 100644 src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj create mode 100644 src/Migrator.Tool/Program.cs create mode 100644 src/Migrator/DatabaseMigrationLock.cs create mode 100644 src/Migrator/MigrationSqlPreview.cs diff --git a/Migrator.slnx b/Migrator.slnx index 4cddff02..cf4bc016 100644 --- a/Migrator.slnx +++ b/Migrator.slnx @@ -13,6 +13,14 @@ + + + + + + + + diff --git a/src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj b/src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj new file mode 100644 index 00000000..e8f1027b --- /dev/null +++ b/src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj @@ -0,0 +1,9 @@ + + net9.09.0.0MPL-1.1Optional dependency injection, options and logging integration for Migrator.NET. + + + + + + + diff --git a/src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs b/src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs new file mode 100644 index 00000000..df2c56d4 --- /dev/null +++ b/src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Extensions.Logging; +namespace DotNetProjects.Migrator.Extensions.DependencyInjection; + +/// Logs lifecycle events. SQL and exception messages may contain secrets and are omitted. +public sealed class MigrationLogger(Microsoft.Extensions.Logging.ILogger logger) : Framework.ILogger +{ + public void Started(List currentVersion, long finalVersion) => logger.LogInformation("Migration run started; target {Version}", finalVersion); + public void Finished(List currentVersion, long finalVersion) => logger.LogInformation("Migration run completed; target {Version}", finalVersion); + public void MigrateUp(long version, string migrationName) => logger.LogInformation("Applying migration {Version} ({Name})", version, migrationName); + public void MigrateDown(long version, string migrationName) => logger.LogInformation("Reverting migration {Version} ({Name})", version, migrationName); + public void Skipping(long version) => logger.LogWarning("Skipping migration {Version}", version); + public void RollingBack(long originalVersion) => logger.LogWarning("Rolling back migration {Version}", originalVersion); + public void ApplyingDBChange(string sql) => logger.LogDebug("Executing a database change"); + public void Exception(long version, string migrationName, Exception ex) => logger.LogError("Migration {Version} failed: {ExceptionType}", version, ex.GetType().Name); + public void Exception(string message, Exception ex) => logger.LogError("Migration operation failed: {ExceptionType}", ex.GetType().Name); + public void Log(string format, params object[] args) => logger.LogInformation("{Message}", string.Format(CultureInfo.InvariantCulture, format, args)); + public void Warn(string format, params object[] args) => logger.LogWarning("{Message}", string.Format(CultureInfo.InvariantCulture, format, args)); + public void Trace(string format, params object[] args) { } // Provider traces commonly contain SQL values. +} diff --git a/src/Migrator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Migrator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..0ae41213 --- /dev/null +++ b/src/Migrator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Reflection; +using DotNetProjects.Migrator.Framework; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +namespace DotNetProjects.Migrator.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddMigrator(this IServiceCollection services, + Func providerFactory, Assembly migrations, Action configure = null) + { + services.AddOptions(); + if (configure != null) services.Configure(configure); + services.AddScoped(providerFactory); + foreach (var type in MigrationLoader.GetMigrationTypes(migrations)) services.TryAddTransient(type); + services.AddScoped(sp => + { + var provider = sp.GetRequiredService(); + var loader = new MigrationLoader(provider, migrations, false); + var options = sp.GetRequiredService>().Value; + options.Activator ??= type => (IMigration)sp.GetRequiredService(type); + return new Migrator(provider, new MigrationLogger(sp.GetService()?.CreateLogger("Migrator.NET") ?? NullLogger.Instance), loader) { Options = options }; + }); + return services; + } +} diff --git a/src/Migrator.Tests/DatabaseLockTests.cs b/src/Migrator.Tests/DatabaseLockTests.cs new file mode 100644 index 00000000..834761a9 --- /dev/null +++ b/src/Migrator.Tests/DatabaseLockTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Data.Common; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Providers; +using Migrator.Tests.Settings; +using NUnit.Framework; +namespace Migrator.Tests; + +[TestFixture(ProviderTypes.SqlServer, Category = "SQLServer")] +[TestFixture(ProviderTypes.PostgreSQL, Category = "PostgreSQL")] +[TestFixture(ProviderTypes.Mysql, Category = "MySQL")] +[TestFixture(ProviderTypes.MariaDB, Category = "MariaDB")] +public class DatabaseLockTests(ProviderTypes type) +{ + private DbConnection Open() + { + DbConnection connection; + if (type == ProviderTypes.SqlServer) + { + var config = new ConfigurationReader().GetDatabaseConnectionConfigById("SQLServer"); + var builder = new Microsoft.Data.SqlClient.SqlConnectionStringBuilder(config.ConnectionString) { InitialCatalog = "master" }; + connection = new Microsoft.Data.SqlClient.SqlConnection(builder.ConnectionString); + } + else if (type == ProviderTypes.PostgreSQL) + connection = new Npgsql.NpgsqlConnection(new ConfigurationReader().GetDatabaseConnectionConfigById("PostgreSQL").ConnectionString); + else + connection = new MySql.Data.MySqlClient.MySqlConnection(Environment.GetEnvironmentVariable(type == ProviderTypes.Mysql ? "MIGRATOR_MYSQL" : "MIGRATOR_MARIADB") + ?? "Server=127.0.0.1;Database=testdb;User ID=root;Password=rootpass;Pooling=false"); + connection.Open(); return connection; + } + [Test] public void IndependentSessionsContendAndCanAcquireAfterRelease() + { + using var connection1 = Open(); using var connection2 = Open(); + using var p1 = ProviderFactory.Create(type, connection1, null); + using var p2 = ProviderFactory.Create(type, connection2, null); + var migrationLock = new DatabaseMigrationLock(); var scope = Guid.NewGuid().ToString("N"); + using (migrationLock.Acquire(p1, scope, TimeSpan.FromSeconds(1))) + { + Assert.Throws(() => migrationLock.Acquire(p2, scope, TimeSpan.FromMilliseconds(100))); + using var independentScope = migrationLock.Acquire(p2, scope + "other", TimeSpan.Zero); + } + using var acquiredAfterRelease = migrationLock.Acquire(p2, scope, TimeSpan.FromSeconds(1)); + } +} diff --git a/src/Migrator.Tests/Migrator.Tests.csproj b/src/Migrator.Tests/Migrator.Tests.csproj index b1cc09e6..c6c1926f 100644 --- a/src/Migrator.Tests/Migrator.Tests.csproj +++ b/src/Migrator.Tests/Migrator.Tests.csproj @@ -45,6 +45,8 @@ + + diff --git a/src/Migrator.Tests/RunnerFeatureTests.cs b/src/Migrator.Tests/RunnerFeatureTests.cs index ddd3fb30..6f165835 100644 --- a/src/Migrator.Tests/RunnerFeatureTests.cs +++ b/src/Migrator.Tests/RunnerFeatureTests.cs @@ -100,6 +100,32 @@ [Test] public void LockPrecedesHistoryAndReleasesOnFailure() Assert.Throws(() => runner.MigrateToLastVersion()); Assert.That(migrationLock.Disposed, Is.True); } + [Test] public void LegacyPreviewRequiresOptInAndNeverCreatesHistory() + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(First)); + Assert.Throws(() => runner.PreviewSql(1, ProviderTypes.SQLite)); + Assert.That(Events, Is.Empty); + var sql = runner.PreviewSql(1, ProviderTypes.SQLite, allowLegacyBodies: true); + Assert.That(sql, Does.Contain("CREATE TABLE")); + Assert.That(p.TableExists("First"), Is.False); + Assert.That(p.TableExists(p.SchemaInfoTable), Is.False); + Assert.That(Events, Is.EqualTo(new[] { "first" })); // Opt-in still executes arbitrary C#. + } + [Migration(1)] internal class DirectConnection : Migration + { + public override void Up() => _ = Database.Connection; + public override void Down() => throw new NotSupportedException(); + } + [Test] public void LegacyPreviewRejectsDirectConnectionsAndUnsupportedLocks() + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(DirectConnection)); + Assert.Throws(() => runner.PreviewSql(1, ProviderTypes.SQLite, true)); + runner.Options.Lock = new DatabaseMigrationLock(); + Assert.Throws(() => runner.MigrateTo(1)); + Assert.That(p.TableExists(p.SchemaInfoTable), Is.False); + } private sealed class ProbeLock : IMigrationLock, IDisposable { public bool Disposed { get; private set; } diff --git a/src/Migrator.Tests/ToolingTests.cs b/src/Migrator.Tests/ToolingTests.cs new file mode 100644 index 00000000..fe3a9190 --- /dev/null +++ b/src/Migrator.Tests/ToolingTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Data; +using System.IO; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Extensions.DependencyInjection; +using DotNetProjects.Migrator.Providers; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +namespace Migrator.Tests; + +public class ToolingTests +{ + public sealed class Dependency { public bool Activated { get; set; } } + [Migration(900001, Scope = "tooling-spec")] + public class InjectedMigration(Dependency dependency) : Migration + { + public override void Up() { dependency.Activated = true; Database.AddTable("Injected", new Column("Id", DbType.Int32)); } + public override void Down() => Database.RemoveTable("Injected"); + } + [Test, Category("SQLite")] + public void DependencyInjectionResolvesConstructorAndOptions() + { + var services = new ServiceCollection(); var dependency = new Dependency(); + services.AddSingleton(dependency); + services.AddMigrator(_ => ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null, "tooling-spec"), typeof(ToolingTests).Assembly, + options => options.TransactionMode = MigrationTransactionMode.WholeSession); + using var container = services.BuildServiceProvider(); using var scope = container.CreateScope(); + var runner = scope.ServiceProvider.GetRequiredService(); + runner.MigrateToLastVersion(); + Assert.That(dependency.Activated, Is.True); + Assert.That(scope.ServiceProvider.GetRequiredService().TableExists("Injected"), Is.True); + } + [TestCase(new[] { "bad-command" }, 2)] + [TestCase(new[] { "--help" }, 0)] + [TestCase(new[] { "rollback", "--provider", "SQLite" }, 2)] + public void CliReturnsMeaningfulArgumentExitCodes(string[] args, int exit) + { + using var output = new StringWriter(); using var error = new StringWriter(); + Assert.That(MigratorCommand.Run(args, output, error), Is.EqualTo(exit)); + } + [Test] public void CliCanListWithoutOpeningDatabase() + { + using var output = new StringWriter(); using var error = new StringWriter(); + var exit = MigratorCommand.Run(new[] { "list", "--assembly", typeof(ToolingTests).Assembly.Location, "--provider", "SQLite", "--scope", "tooling-spec" }, output, error); + Assert.That(exit, Is.Zero, error.ToString()); + Assert.That(output.ToString(), Does.Contain("900001")); + } +} diff --git a/src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj b/src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj new file mode 100644 index 00000000..0a0f2501 --- /dev/null +++ b/src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj @@ -0,0 +1,12 @@ + + Exenet9.0enabletruemigratorDotNetProjects.Migrator.Tool9.0.0MPL-1.1 + + + + + + + + + + diff --git a/src/Migrator.Tool/Program.cs b/src/Migrator.Tool/Program.cs new file mode 100644 index 00000000..540ace85 --- /dev/null +++ b/src/Migrator.Tool/Program.cs @@ -0,0 +1,115 @@ +using System.Reflection; +using System.Runtime.Loader; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Loggers; +using DotNetProjects.Migrator.Providers; + +return MigratorCommand.Run(args, Console.Out, Console.Error); + +public static class MigratorCommand +{ + public static int Run(string[] args, TextWriter output, TextWriter error) + { + try { return Execute(args, output); } + catch (ArgumentException ex) { error.WriteLine("Invalid arguments: " + ex.ParamName + ". Use --help."); return 2; } + catch (NotSupportedException) { error.WriteLine("The requested operation is unsupported by this provider or preview mode."); return 3; } + catch (TimeoutException) { error.WriteLine("Migration lock acquisition timed out."); return 4; } + catch (Exception ex) { error.WriteLine("Migration command failed (" + ex.GetType().Name + "). Exception details are omitted because they may contain credentials or SQL values."); return 1; } + } + private static int Execute(string[] args, TextWriter output) + { + if (args.Length == 0 || args.Contains("--help")) + { + output.WriteLine("migrator --assembly PATH --provider NAME"); + output.WriteLine("--connection-env NAME (default MIGRATOR_CONNECTION), --scope NAME, --schema NAME, --target VERSION"); + output.WriteLine("--tags a,b --tag-match Any|All --profiles a,b --transaction PerMigration|None|WholeSession"); + output.WriteLine("--timeout SECONDS --lock --lock-timeout SECONDS --output PATH --offline --allow-legacy-preview"); + output.WriteLine("rollback requires --target. Offline SQL assumes empty history. Legacy preview executes trusted arbitrary C#."); + return 0; + } + var command = args[0]; + if (!new[] { "list", "status", "validate", "migrate", "rollback", "plan", "sql" }.Contains(command)) throw new ArgumentException(null, "command"); + var values = new Dictionary(StringComparer.Ordinal); + var flags = new HashSet { "--lock", "--offline", "--allow-legacy-preview" }; + var allowed = new HashSet { "--assembly", "--provider", "--connection-env", "--scope", "--schema", "--target", "--tags", "--tag-match", "--profiles", "--transaction", "--timeout", "--lock-timeout", "--output" }; + for (var i = 1; i < args.Length; i++) + { + var key = args[i]; + if (values.ContainsKey(key)) throw new ArgumentException(null, key); + if (flags.Contains(key)) values.Add(key, "true"); + else if (allowed.Contains(key) && i + 1 < args.Length && !args[i + 1].StartsWith("--")) values.Add(key, args[++i]); + else throw new ArgumentException(null, key); + } + string Value(string key, string fallback = null) => values.GetValueOrDefault(key, fallback); + T EnumValue(string key, string fallback) where T : struct, Enum => Enum.TryParse(Value(key, fallback), true, out var result) && Enum.IsDefined(result) ? result : throw new ArgumentException(null, key); + var providerType = EnumValue("--provider", "none"); + if (providerType == ProviderTypes.none) throw new ArgumentException(null, "--provider"); + var assemblyPath = Path.GetFullPath(Value("--assembly") ?? throw new ArgumentException(null, "--assembly")); + var resolver = new AssemblyDependencyResolver(assemblyPath); + Assembly Resolving(AssemblyLoadContext context, AssemblyName name) + { + var path = resolver.ResolveAssemblyToPath(name); + return path == null ? null : context.LoadFromAssemblyPath(path); + } + AssemblyLoadContext.Default.Resolving += Resolving; + try + { + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); + var scope = Value("--scope", "default"); + var types = MigrationLoader.GetMigrationTypes(assembly).Where(t => + (t.GetCustomAttribute()?.Scope ?? t.GetCustomAttribute()?.Scope ?? t.GetCustomAttribute()?.Scope) is not string ownScope || ownScope == scope).ToArray(); + var tags = Value("--tags", "").Split(',', StringSplitOptions.RemoveEmptyEntries); + var tagMatch = EnumValue("--tag-match", "Any"); + bool Selected(Type t) + { + var own = t.GetCustomAttribute()?.Tags ?? Array.Empty(); + return tags.Length == 0 || (tagMatch == TagMatchMode.All ? tags.All(own.Contains) : tags.Any(own.Contains)); + } + var versioned = types.Where(t => t.GetCustomAttribute() != null && Selected(t)).OrderBy(MigrationLoader.GetMigrationVersion).ToArray(); + var target = Value("--target") is { } targetString ? long.TryParse(targetString, out var parsed) && parsed >= 0 ? parsed : throw new ArgumentException(null, "--target") : versioned.Select(MigrationLoader.GetMigrationVersion).DefaultIfEmpty(0).Max(); + if (command == "rollback" && !values.ContainsKey("--target")) throw new ArgumentException(null, "--target"); + if (command == "list") + { + foreach (var type in versioned) output.WriteLine(MigrationLoader.GetMigrationVersion(type) + " " + type.FullName); + return 0; + } + if (values.ContainsKey("--offline")) + { + if (command != "sql" || values.ContainsKey("--profiles") || types.Any(t => t.GetCustomAttribute() != null)) throw new NotSupportedException(); + var plan = MigrationPlanner.Create(versioned.Select(MigrationLoader.GetMigrationVersion), Array.Empty(), target); + var migrations = plan.Select(step => ((IMigration)Activator.CreateInstance(versioned.Single(t => MigrationLoader.GetMigrationVersion(t) == step.Version)), step.IsUp)); + Write(MigrationSqlPreview.Generate(providerType, migrations, values.ContainsKey("--allow-legacy-preview"))); + return 0; + } + var connectionString = Environment.GetEnvironmentVariable(Value("--connection-env", "MIGRATOR_CONNECTION")) ?? throw new ArgumentException(null, "--connection-env"); + var providerName = providerType switch + { + ProviderTypes.SQLite => "Microsoft.Data.Sqlite", ProviderTypes.SqlServer or ProviderTypes.SqlServer2005 => "Microsoft.Data.SqlClient", + ProviderTypes.PostgreSQL or ProviderTypes.PostgreSQL82 => "Npgsql", ProviderTypes.Mysql or ProviderTypes.MariaDB => "MySql.Data.MySqlClient", + ProviderTypes.Oracle => "Oracle.ManagedDataAccess.Client", ProviderTypes.Firebird => "FirebirdSql.Data.FirebirdClient", + _ => throw new NotSupportedException() + }; + using var provider = ProviderFactory.Create(providerType, connectionString, Value("--schema"), scope, providerName); + if (values.ContainsKey("--timeout")) provider.CommandTimeout = Seconds("--timeout", "30"); + var runner = new Migrator(provider, false, new Logger(false), types); + runner.Options.Tags.UnionWith(tags); runner.Options.TagMatch = tagMatch; + runner.Options.Profiles.UnionWith(Value("--profiles", "").Split(',', StringSplitOptions.RemoveEmptyEntries)); + runner.Options.TransactionMode = EnumValue("--transaction", "PerMigration"); + if (values.ContainsKey("--lock")) runner.Options.Lock = new DatabaseMigrationLock(); + runner.Options.LockTimeout = TimeSpan.FromSeconds(Seconds("--lock-timeout", "30")); + switch (command) + { + case "status": foreach (var applied in ((IMigrationHistory)provider).ReadAppliedMigrations()) output.WriteLine(applied + " applied"); break; + case "validate": _ = runner.Plan(target); output.WriteLine("Migration plan is valid."); break; + case "plan": foreach (var step in runner.Plan(target)) output.WriteLine(step.Version + (step.IsUp ? " up" : " down")); break; + case "sql": Write(runner.PreviewSql(target, providerType, values.ContainsKey("--allow-legacy-preview"))); break; + default: runner.MigrateTo(target); output.WriteLine("Migration completed."); break; + } + return 0; + int Seconds(string key, string fallback) => int.TryParse(Value(key, fallback), out var seconds) && seconds >= 0 ? seconds : throw new ArgumentException(null, key); + void Write(string sql) { if (Value("--output") is { } path) File.WriteAllText(path, sql); else output.WriteLine(sql); } + } + finally { AssemblyLoadContext.Default.Resolving -= Resolving; } + } +} diff --git a/src/Migrator/DatabaseMigrationLock.cs b/src/Migrator/DatabaseMigrationLock.cs new file mode 100644 index 00000000..d2ae7bd3 --- /dev/null +++ b/src/Migrator/DatabaseMigrationLock.cs @@ -0,0 +1,76 @@ +using System; +using System.Buffers.Binary; +using System.Data; +using System.Diagnostics; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Impl.Mysql; +using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; +using DotNetProjects.Migrator.Providers.Impl.SqlServer; +namespace DotNetProjects.Migrator; + +/// Session-owned database locks for SQL Server, PostgreSQL and MySQL/MariaDB. +public sealed class DatabaseMigrationLock : IMigrationLock +{ + public IDisposable Acquire(ITransformationProvider provider, string scope, TimeSpan timeout) + { + if (timeout < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(timeout)); + var kind = provider.Dialect switch + { + SqlServerDialect => 0, PostgreSQLDialect => 1, MysqlDialect => 2, + _ => throw new NotSupportedException("Database migration locking is supported on SQL Server, PostgreSQL and MySQL/MariaDB.") + }; + var connection = provider.Connection; + if (connection.State != ConnectionState.Open) throw new MigrationException("Migration locking requires an open connection."); + var resource = "Migrator.NET:" + connection.Database + ":" + provider.SchemaInfoTable + ":" + scope; + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(resource)); + object key = kind == 1 ? BinaryPrimitives.ReadInt64BigEndian(hash) : Convert.ToHexString(hash); + var acquire = kind switch + { + 0 => "DECLARE @result int; EXEC @result=sys.sp_getapplock @Resource=@key, @LockMode='Exclusive', @LockOwner='Session', @LockTimeout=0; SELECT @result", + 1 => "SELECT pg_try_advisory_lock(@key)", + _ => "SELECT GET_LOCK(@key, 0)" + }; + var release = kind switch + { + 0 => "DECLARE @result int; EXEC @result=sys.sp_releaseapplock @Resource=@key, @LockOwner='Session'; SELECT @result", + 1 => "SELECT pg_advisory_unlock(@key)", + _ => "SELECT RELEASE_LOCK(@key)" + }; + var watch = Stopwatch.StartNew(); + while (true) + { + var value = Scalar(connection, acquire, key); + if (value == null || value == DBNull.Value) throw new MigrationException("Database lock acquisition returned no result."); + var code = Convert.ToInt32(value, CultureInfo.InvariantCulture); + if (kind == 0 ? code >= 0 : code == 1) return new Lease(connection, release, key, kind); + if (kind == 0 && code != -1) throw new MigrationException("Database lock acquisition failed with code " + code); + if (watch.Elapsed >= timeout) throw new TimeoutException("Timed out acquiring the migration lock."); + Thread.Sleep((int)Math.Min(50, Math.Max(1, (timeout - watch.Elapsed).TotalMilliseconds))); + } + } + private static object Scalar(IDbConnection connection, string sql, object key) + { + using var command = connection.CreateCommand(); + command.CommandText = sql; command.CommandTimeout = 30; + var parameter = command.CreateParameter(); parameter.ParameterName = "@key"; parameter.Value = key; + parameter.DbType = key is long ? DbType.Int64 : DbType.String; + command.Parameters.Add(parameter); + return command.ExecuteScalar(); + } + private sealed class Lease(IDbConnection connection, string release, object key, int kind) : IDisposable + { + private bool disposed; + public void Dispose() + { + if (disposed) return; + var value = Scalar(connection, release, key); + if (value == null || value == DBNull.Value || (kind == 0 ? Convert.ToInt32(value) < 0 : Convert.ToInt32(value) != 1)) + throw new MigrationException("The database did not confirm migration lock release."); + disposed = true; + } + } +} diff --git a/src/Migrator/MigrationSqlPreview.cs b/src/Migrator/MigrationSqlPreview.cs new file mode 100644 index 00000000..cf32793b --- /dev/null +++ b/src/Migrator/MigrationSqlPreview.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Fluent; +using DotNetProjects.Migrator.Providers; +namespace DotNetProjects.Migrator; + +public static class MigrationSqlPreview +{ + /// Generates SQL without a database. C# authoring code still executes and must be trusted. + public static string Generate(ProviderTypes provider, IEnumerable<(IMigration Migration, bool Up)> migrations, + bool allowLegacyBodies = false, Func existingTables = null) + { + var context = new SqlGenerationContext(provider, existingTables); + var sql = new List(); + foreach (var (migration, up) in migrations) + { + var original = migration.Database; + var proxy = DispatchProxy.Create(); + var recorder = (PreviewProvider)(object)proxy; + try + { + migration.Database = proxy; + IReadOnlyList operations; + if (migration is FluentMigration fluent) operations = fluent.GetOperations(up); + else + { + if (!allowLegacyBodies) throw new NotSupportedException("Imperative SQL preview requires explicit allowLegacyBodies opt-in. Arbitrary C# cannot be sandboxed."); + if (up) migration.Up(); else migration.Down(); + operations = recorder.Operations; + } + foreach (var operation in operations) sql.Add(operation.ToSql(context)); + } + finally { migration.Database = original; } + } + return string.Join(Environment.NewLine, sql.Where(s => !string.IsNullOrWhiteSpace(s))); + } + + // Every method is denied unless explicitly mapped to a captured operation. No connection is exposed. + public class PreviewProvider : DispatchProxy + { + internal readonly List Operations = new(); + protected override object Invoke(MethodInfo method, object[] args) + { + MigrationOperation operation = method.Name switch + { + "AddTable" when args.Length == 2 && args[1] is IDbField[] fields => new CreateTableOperation((string)args[0], null, fields.Select(Definitions.Copy).ToArray()), + "AddColumn" when args.Length == 2 && args[1] is Column column => new ColumnOperation((string)args[0], Definitions.CopyColumn(column)), + "RemoveTable" => new RemoveOperation(RemoveKind.Table, (string)args[0]), + "RenameTable" => new RenameOperation((string)args[0], (string)args[1]), + "RenameColumn" => new RenameOperation((string)args[0], (string)args[2], (string)args[1]), + "Insert" when args.Length == 3 && args[1] is string[] columns && args[2] is object[] values => new DataOperation(DataKind.Insert, (string)args[0], (string[])columns.Clone(), (object[])values.Clone()), + "ExecuteNonQuery" when args.Length == 1 => new SqlOperation((string)args[0]), + _ => throw new NotSupportedException("SQL preview blocks provider member " + method.Name + ". Use a structured operation or an explicit SQL script.") + }; + Operations.Add(operation); + return method.ReturnType == typeof(int) ? 0 : null; + } + } +} diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index 1a2fe0fb..1c52760a 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -26,7 +26,7 @@ namespace DotNetProjects.Migrator; /// public class Migrator { - public RunnerOptions Options { get; } = new(); + public RunnerOptions Options { get; init; } = new(); private readonly MigrationLoader _migrationLoader; private readonly ITransformationProvider _provider; @@ -220,6 +220,35 @@ public IReadOnlyList Plan(long version) return CreatePlan(history.ReadAppliedMigrations(), version); } + public string PreviewSql(long version, ProviderTypes provider, bool allowLegacyBodies = false) + { + _migrationLoader.Activator = Options.Activator; + var plan = Plan(version); + var migrations = new List<(IMigration, bool)>(); + void AddMaintenance(MaintenanceStage stage) + { + foreach (var type in _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } a && a.Stage == stage && _migrationLoader.InScope(a.Scope)) + .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal)) + migrations.Add((_migrationLoader.CreateInstance(type), true)); + } + AddMaintenance(MaintenanceStage.BeforeRun); + foreach (var step in plan) + { + AddMaintenance(MaintenanceStage.BeforeMigration); + migrations.Add((_migrationLoader.GetMigration(step.Version), step.IsUp)); + AddMaintenance(MaintenanceStage.AfterMigration); + } + foreach (var name in Options.Profiles) + if (!_migrationLoader.AuxiliaryTypes.Any(t => t.GetCustomAttribute() is { } a && a.Name == name && _migrationLoader.InScope(a.Scope))) + throw new MigrationException("Unknown profile: " + name); + foreach (var type in _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } a && Options.Profiles.Contains(a.Name) && _migrationLoader.InScope(a.Scope)) + .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal)) + migrations.Add((_migrationLoader.CreateInstance(type), true)); + AddMaintenance(MaintenanceStage.AfterRun); + return MigrationSqlPreview.Generate(provider, migrations, allowLegacyBodies, + table => _provider.TableExists(table) ? _provider.GetColumns(table) : throw new MigrationException("Preview table does not exist: " + table)); + } + public void MigrateTo(long version) { if (DryRun) From 9730079a2d4c9a6117022b98ab2004b1313ea532 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:14:28 +0200 Subject: [PATCH 03/17] Register packaged CLI database factories before connecting A locally installed CLI could list and generate offline SQL but failed to connect because the core factory fallback selected its historical driver assembly. Register each bundled driver's factory explicitly before creating a provider. Validation: rebuilt solution; connected SQLite CLI migrate/status/rollback smoke passed. Added a CLI regression with a disposable file database that verifies version history and final table removal; Unit 83 and SQLite 161 passed. --- src/Migrator.Tests/ToolingTests.cs | 29 +++++++++++++++++++++++++++++ src/Migrator.Tool/Program.cs | 10 ++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Migrator.Tests/ToolingTests.cs b/src/Migrator.Tests/ToolingTests.cs index fe3a9190..27d1a6de 100644 --- a/src/Migrator.Tests/ToolingTests.cs +++ b/src/Migrator.Tests/ToolingTests.cs @@ -39,6 +39,35 @@ public void CliReturnsMeaningfulArgumentExitCodes(string[] args, int exit) using var output = new StringWriter(); using var error = new StringWriter(); Assert.That(MigratorCommand.Run(args, output, error), Is.EqualTo(exit)); } + [Migration(900002, Scope = "cli-spec")] + public class CliMigration : DotNetProjects.Migrator.Framework.Fluent.AutoReversingMigration + { + public override void BuildUp(DotNetProjects.Migrator.Framework.Fluent.MigrationBuilder migration) + => migration.Create.Table("CliExample").WithColumn("Id").AsInt32(); + } + [Test, Category("SQLite")] + public void CliMigratesReadsStatusAndRollsBackWithPackagedDriver() + { + var file = Path.Combine(Path.GetTempPath(), "migrator-cli-" + Guid.NewGuid().ToString("N") + ".db"); + var environmentName = "MIGRATOR_TEST_" + Guid.NewGuid().ToString("N"); + Environment.SetEnvironmentVariable(environmentName, "Data Source=" + file + ";Pooling=False"); + try + { + foreach (var command in new[] { "migrate", "status", "rollback" }) + { + using var output = new StringWriter(); using var error = new StringWriter(); + var args = new System.Collections.Generic.List { command, "--assembly", typeof(ToolingTests).Assembly.Location, "--provider", "SQLite", "--scope", "cli-spec", "--connection-env", environmentName }; + if (command == "rollback") args.AddRange(new[] { "--target", "0" }); + Assert.That(MigratorCommand.Run(args.ToArray(), output, error), Is.Zero, error.ToString()); + if (command == "status") Assert.That(output.ToString(), Does.Contain("900002 applied")); + } + using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=" + file + ";Pooling=False"); connection.Open(); + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null, "cli-spec"); + Assert.That(provider.TableExists("CliExample"), Is.False); + Assert.That(((IMigrationHistory)provider).ReadAppliedMigrations(), Is.Empty); + } + finally { Environment.SetEnvironmentVariable(environmentName, null); File.Delete(file); } + } [Test] public void CliCanListWithoutOpeningDatabase() { using var output = new StringWriter(); using var error = new StringWriter(); diff --git a/src/Migrator.Tool/Program.cs b/src/Migrator.Tool/Program.cs index 540ace85..2fad5d83 100644 --- a/src/Migrator.Tool/Program.cs +++ b/src/Migrator.Tool/Program.cs @@ -90,6 +90,16 @@ bool Selected(Type t) ProviderTypes.Oracle => "Oracle.ManagedDataAccess.Client", ProviderTypes.Firebird => "FirebirdSql.Data.FirebirdClient", _ => throw new NotSupportedException() }; + System.Data.Common.DbProviderFactories.RegisterFactory(providerName, providerType switch + { + ProviderTypes.SQLite => Microsoft.Data.Sqlite.SqliteFactory.Instance, + ProviderTypes.SqlServer or ProviderTypes.SqlServer2005 => Microsoft.Data.SqlClient.SqlClientFactory.Instance, + ProviderTypes.PostgreSQL or ProviderTypes.PostgreSQL82 => Npgsql.NpgsqlFactory.Instance, + ProviderTypes.Mysql or ProviderTypes.MariaDB => MySql.Data.MySqlClient.MySqlClientFactory.Instance, + ProviderTypes.Oracle => Oracle.ManagedDataAccess.Client.OracleClientFactory.Instance, + ProviderTypes.Firebird => FirebirdSql.Data.FirebirdClient.FirebirdClientFactory.Instance, + _ => throw new NotSupportedException() + }); using var provider = ProviderFactory.Create(providerType, connectionString, Value("--schema"), scope, providerName); if (values.ContainsKey("--timeout")) provider.CommandTimeout = Seconds("--timeout", "30"); var runner = new Migrator(provider, false, new Logger(false), types); From fedf79ff9c1326d2ad4ae08dda5bc71c14b82956 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:29:02 +0200 Subject: [PATCH 04/17] Harden tooling error boundaries, preview lifecycle and logging Omit free-form provider Log/Warn text so SQL and secrets cannot escape through the optional logging adapter. Distinguish CLI parser and known capability/lock errors from arbitrary migration-body failures. Reject InitializeOnce overrides in preview rather than executing initialization hooks. Pass independent initial-history snapshots to Started/Finished logging. Validation: rebuilt solution; Unit 86 and SQLite 167 passed. Added regressions for three migration exception types returning execution exit code 1, secret/brace logging, initialization-dependent preview rejection and stable lifecycle history arguments. Addresses all four initial PR 177 review findings. --- .../MigrationLogger.cs | 4 +- src/Migrator.Tests/RunnerFeatureTests.cs | 30 ++++++++++++-- src/Migrator.Tests/ToolingTests.cs | 39 +++++++++++++++++++ src/Migrator.Tool/Program.cs | 33 ++++++++-------- src/Migrator/DatabaseMigrationLock.cs | 2 +- src/Migrator/MigrationSqlPreview.cs | 14 +++++-- src/Migrator/Migrator.cs | 14 +++++-- src/Migrator/RunnerOptions.cs | 9 +++++ 8 files changed, 116 insertions(+), 29 deletions(-) diff --git a/src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs b/src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs index df2c56d4..cdc63c2d 100644 --- a/src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs +++ b/src/Migrator.Extensions.DependencyInjection/MigrationLogger.cs @@ -16,7 +16,7 @@ public sealed class MigrationLogger(Microsoft.Extensions.Logging.ILogger logger) public void ApplyingDBChange(string sql) => logger.LogDebug("Executing a database change"); public void Exception(long version, string migrationName, Exception ex) => logger.LogError("Migration {Version} failed: {ExceptionType}", version, ex.GetType().Name); public void Exception(string message, Exception ex) => logger.LogError("Migration operation failed: {ExceptionType}", ex.GetType().Name); - public void Log(string format, params object[] args) => logger.LogInformation("{Message}", string.Format(CultureInfo.InvariantCulture, format, args)); - public void Warn(string format, params object[] args) => logger.LogWarning("{Message}", string.Format(CultureInfo.InvariantCulture, format, args)); + public void Log(string format, params object[] args) => logger.LogInformation("Provider informational event"); + public void Warn(string format, params object[] args) => logger.LogWarning("Provider warning event"); public void Trace(string format, params object[] args) { } // Provider traces commonly contain SQL values. } diff --git a/src/Migrator.Tests/RunnerFeatureTests.cs b/src/Migrator.Tests/RunnerFeatureTests.cs index 6f165835..d31c4620 100644 --- a/src/Migrator.Tests/RunnerFeatureTests.cs +++ b/src/Migrator.Tests/RunnerFeatureTests.cs @@ -104,7 +104,7 @@ [Test] public void LegacyPreviewRequiresOptInAndNeverCreatesHistory() { using var p = Provider(); var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(First)); - Assert.Throws(() => runner.PreviewSql(1, ProviderTypes.SQLite)); + Assert.Catch(() => runner.PreviewSql(1, ProviderTypes.SQLite)); Assert.That(Events, Is.Empty); var sql = runner.PreviewSql(1, ProviderTypes.SQLite, allowLegacyBodies: true); Assert.That(sql, Does.Contain("CREATE TABLE")); @@ -121,11 +121,35 @@ [Test] public void LegacyPreviewRejectsDirectConnectionsAndUnsupportedLocks() { using var p = Provider(); var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(DirectConnection)); - Assert.Throws(() => runner.PreviewSql(1, ProviderTypes.SQLite, true)); + Assert.Catch(() => runner.PreviewSql(1, ProviderTypes.SQLite, true)); runner.Options.Lock = new DatabaseMigrationLock(); - Assert.Throws(() => runner.MigrateTo(1)); + Assert.Catch(() => runner.MigrateTo(1)); Assert.That(p.TableExists(p.SchemaInfoTable), Is.False); } + [Migration(4)] internal class RequiresInitialization : First + { + public override void InitializeOnce(string[] args) => throw new Exception("must not execute"); + } + [Test] public void PreviewRejectsInitializationDependentMigrationsBeforeBody() + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(RequiresInitialization)); + Assert.Throws(() => runner.PreviewSql(4, ProviderTypes.SQLite, true)); + Assert.That(Events, Is.Empty); + Assert.That(p.TableExists(p.SchemaInfoTable), Is.False); + } + [Test] public void LifecycleLogArgumentsRemainInitialHistorySnapshots() + { + using var p = Provider(); + var logger = NSubstitute.Substitute.For(); + List started = null, finished = null; + logger.Started(NSubstitute.Arg.Do>(h => started = h), NSubstitute.Arg.Any()); + logger.Finished(NSubstitute.Arg.Do>(h => finished = h), NSubstitute.Arg.Any()); + var runner = new DotNetProjects.Migrator.Migrator(p, false, logger, typeof(First)); + runner.MigrateTo(1); + Assert.That(started, Is.Empty); Assert.That(finished, Is.Empty); + Assert.That(p.AppliedMigrations, Is.EqualTo(new long[] { 1 })); + } private sealed class ProbeLock : IMigrationLock, IDisposable { public bool Disposed { get; private set; } diff --git a/src/Migrator.Tests/ToolingTests.cs b/src/Migrator.Tests/ToolingTests.cs index 27d1a6de..b1da945b 100644 --- a/src/Migrator.Tests/ToolingTests.cs +++ b/src/Migrator.Tests/ToolingTests.cs @@ -1,12 +1,14 @@ using System; using System.Data; using System.IO; +using System.Linq; using DotNetProjects.Migrator; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Extensions.DependencyInjection; using DotNetProjects.Migrator.Providers; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using NSubstitute; namespace Migrator.Tests; public class ToolingTests @@ -68,6 +70,43 @@ public void CliMigratesReadsStatusAndRollsBackWithPackagedDriver() } finally { Environment.SetEnvironmentVariable(environmentName, null); File.Delete(file); } } + [Migration(900003, Scope = "cli-errors")] + public class FailingCliMigration : Migration + { + internal static int Kind; + public override void Up() => throw Kind switch + { + 1 => new ArgumentException("SECRET_VALUE"), + 2 => new TimeoutException("SECRET_VALUE"), + _ => new NotSupportedException("SECRET_VALUE") + }; + public override void Down() => throw new NotSupportedException(); + } + [TestCase(1), TestCase(2), TestCase(3), Category("SQLite"), NonParallelizable] + public void CliClassifiesMigrationBodyExceptionsAsExecutionFailure(int kind) + { + var environmentName = "MIGRATOR_TEST_" + Guid.NewGuid().ToString("N"); + Environment.SetEnvironmentVariable(environmentName, "Data Source=:memory:"); + FailingCliMigration.Kind = kind; + try + { + using var output = new StringWriter(); using var error = new StringWriter(); + var exit = MigratorCommand.Run(new[] { "migrate", "--assembly", typeof(ToolingTests).Assembly.Location, "--provider", "SQLite", "--scope", "cli-errors", "--connection-env", environmentName }, output, error); + Assert.That(exit, Is.EqualTo(1)); + Assert.That(error.ToString(), Does.Not.Contain("SECRET_VALUE")); + } + finally { Environment.SetEnvironmentVariable(environmentName, null); } + } + [Test] public void LoggingAdapterOmitsProviderMessagesAndDoesNotFormatSqlBraces() + { + var sink = NSubstitute.Substitute.For(); + var logger = new MigrationLogger(sink); + Assert.DoesNotThrow(() => logger.Log("SECRET_VALUE {")); + logger.Warn("SECRET_VALUE"); logger.Trace("SECRET_VALUE"); logger.ApplyingDBChange("SECRET_VALUE"); + logger.Exception("SECRET_VALUE", new Exception("SECRET_VALUE")); + foreach (var call in sink.ReceivedCalls().Where(c => c.GetMethodInfo().Name == "Log")) + Assert.That(call.GetArguments()[2].ToString(), Does.Not.Contain("SECRET_VALUE")); + } [Test] public void CliCanListWithoutOpeningDatabase() { using var output = new StringWriter(); using var error = new StringWriter(); diff --git a/src/Migrator.Tool/Program.cs b/src/Migrator.Tool/Program.cs index 2fad5d83..a99c177a 100644 --- a/src/Migrator.Tool/Program.cs +++ b/src/Migrator.Tool/Program.cs @@ -12,11 +12,12 @@ public static class MigratorCommand public static int Run(string[] args, TextWriter output, TextWriter error) { try { return Execute(args, output); } - catch (ArgumentException ex) { error.WriteLine("Invalid arguments: " + ex.ParamName + ". Use --help."); return 2; } - catch (NotSupportedException) { error.WriteLine("The requested operation is unsupported by this provider or preview mode."); return 3; } - catch (TimeoutException) { error.WriteLine("Migration lock acquisition timed out."); return 4; } + catch (CliUsageException ex) { error.WriteLine("Invalid arguments: " + ex.Option + ". Use --help."); return 2; } + catch (UnsupportedMigrationFeatureException) { error.WriteLine("The requested operation is unsupported by this provider or preview mode."); return 3; } + catch (MigrationLockTimeoutException) { error.WriteLine("Migration lock acquisition timed out."); return 4; } catch (Exception ex) { error.WriteLine("Migration command failed (" + ex.GetType().Name + "). Exception details are omitted because they may contain credentials or SQL values."); return 1; } } + private sealed class CliUsageException(string option) : Exception { public string Option { get; } = option; } private static int Execute(string[] args, TextWriter output) { if (args.Length == 0 || args.Contains("--help")) @@ -29,23 +30,23 @@ private static int Execute(string[] args, TextWriter output) return 0; } var command = args[0]; - if (!new[] { "list", "status", "validate", "migrate", "rollback", "plan", "sql" }.Contains(command)) throw new ArgumentException(null, "command"); + if (!new[] { "list", "status", "validate", "migrate", "rollback", "plan", "sql" }.Contains(command)) throw new CliUsageException("command"); var values = new Dictionary(StringComparer.Ordinal); var flags = new HashSet { "--lock", "--offline", "--allow-legacy-preview" }; var allowed = new HashSet { "--assembly", "--provider", "--connection-env", "--scope", "--schema", "--target", "--tags", "--tag-match", "--profiles", "--transaction", "--timeout", "--lock-timeout", "--output" }; for (var i = 1; i < args.Length; i++) { var key = args[i]; - if (values.ContainsKey(key)) throw new ArgumentException(null, key); + if (values.ContainsKey(key)) throw new CliUsageException(key); if (flags.Contains(key)) values.Add(key, "true"); else if (allowed.Contains(key) && i + 1 < args.Length && !args[i + 1].StartsWith("--")) values.Add(key, args[++i]); - else throw new ArgumentException(null, key); + else throw new CliUsageException(key); } string Value(string key, string fallback = null) => values.GetValueOrDefault(key, fallback); - T EnumValue(string key, string fallback) where T : struct, Enum => Enum.TryParse(Value(key, fallback), true, out var result) && Enum.IsDefined(result) ? result : throw new ArgumentException(null, key); + T EnumValue(string key, string fallback) where T : struct, Enum => Enum.TryParse(Value(key, fallback), true, out var result) && Enum.IsDefined(result) ? result : throw new CliUsageException(key); var providerType = EnumValue("--provider", "none"); - if (providerType == ProviderTypes.none) throw new ArgumentException(null, "--provider"); - var assemblyPath = Path.GetFullPath(Value("--assembly") ?? throw new ArgumentException(null, "--assembly")); + if (providerType == ProviderTypes.none) throw new CliUsageException("--provider"); + var assemblyPath = Path.GetFullPath(Value("--assembly") ?? throw new CliUsageException("--assembly")); var resolver = new AssemblyDependencyResolver(assemblyPath); Assembly Resolving(AssemblyLoadContext context, AssemblyName name) { @@ -67,8 +68,8 @@ bool Selected(Type t) return tags.Length == 0 || (tagMatch == TagMatchMode.All ? tags.All(own.Contains) : tags.Any(own.Contains)); } var versioned = types.Where(t => t.GetCustomAttribute() != null && Selected(t)).OrderBy(MigrationLoader.GetMigrationVersion).ToArray(); - var target = Value("--target") is { } targetString ? long.TryParse(targetString, out var parsed) && parsed >= 0 ? parsed : throw new ArgumentException(null, "--target") : versioned.Select(MigrationLoader.GetMigrationVersion).DefaultIfEmpty(0).Max(); - if (command == "rollback" && !values.ContainsKey("--target")) throw new ArgumentException(null, "--target"); + var target = Value("--target") is { } targetString ? long.TryParse(targetString, out var parsed) && parsed >= 0 ? parsed : throw new CliUsageException("--target") : versioned.Select(MigrationLoader.GetMigrationVersion).DefaultIfEmpty(0).Max(); + if (command == "rollback" && !values.ContainsKey("--target")) throw new CliUsageException("--target"); if (command == "list") { foreach (var type in versioned) output.WriteLine(MigrationLoader.GetMigrationVersion(type) + " " + type.FullName); @@ -76,19 +77,19 @@ bool Selected(Type t) } if (values.ContainsKey("--offline")) { - if (command != "sql" || values.ContainsKey("--profiles") || types.Any(t => t.GetCustomAttribute() != null)) throw new NotSupportedException(); + if (command != "sql" || values.ContainsKey("--profiles") || types.Any(t => t.GetCustomAttribute() != null)) throw new UnsupportedMigrationFeatureException("CLI operation is unsupported."); var plan = MigrationPlanner.Create(versioned.Select(MigrationLoader.GetMigrationVersion), Array.Empty(), target); var migrations = plan.Select(step => ((IMigration)Activator.CreateInstance(versioned.Single(t => MigrationLoader.GetMigrationVersion(t) == step.Version)), step.IsUp)); Write(MigrationSqlPreview.Generate(providerType, migrations, values.ContainsKey("--allow-legacy-preview"))); return 0; } - var connectionString = Environment.GetEnvironmentVariable(Value("--connection-env", "MIGRATOR_CONNECTION")) ?? throw new ArgumentException(null, "--connection-env"); + var connectionString = Environment.GetEnvironmentVariable(Value("--connection-env", "MIGRATOR_CONNECTION")) ?? throw new CliUsageException("--connection-env"); var providerName = providerType switch { ProviderTypes.SQLite => "Microsoft.Data.Sqlite", ProviderTypes.SqlServer or ProviderTypes.SqlServer2005 => "Microsoft.Data.SqlClient", ProviderTypes.PostgreSQL or ProviderTypes.PostgreSQL82 => "Npgsql", ProviderTypes.Mysql or ProviderTypes.MariaDB => "MySql.Data.MySqlClient", ProviderTypes.Oracle => "Oracle.ManagedDataAccess.Client", ProviderTypes.Firebird => "FirebirdSql.Data.FirebirdClient", - _ => throw new NotSupportedException() + _ => throw new UnsupportedMigrationFeatureException("CLI operation is unsupported.") }; System.Data.Common.DbProviderFactories.RegisterFactory(providerName, providerType switch { @@ -98,7 +99,7 @@ bool Selected(Type t) ProviderTypes.Mysql or ProviderTypes.MariaDB => MySql.Data.MySqlClient.MySqlClientFactory.Instance, ProviderTypes.Oracle => Oracle.ManagedDataAccess.Client.OracleClientFactory.Instance, ProviderTypes.Firebird => FirebirdSql.Data.FirebirdClient.FirebirdClientFactory.Instance, - _ => throw new NotSupportedException() + _ => throw new UnsupportedMigrationFeatureException("CLI operation is unsupported.") }); using var provider = ProviderFactory.Create(providerType, connectionString, Value("--schema"), scope, providerName); if (values.ContainsKey("--timeout")) provider.CommandTimeout = Seconds("--timeout", "30"); @@ -117,7 +118,7 @@ bool Selected(Type t) default: runner.MigrateTo(target); output.WriteLine("Migration completed."); break; } return 0; - int Seconds(string key, string fallback) => int.TryParse(Value(key, fallback), out var seconds) && seconds >= 0 ? seconds : throw new ArgumentException(null, key); + int Seconds(string key, string fallback) => int.TryParse(Value(key, fallback), out var seconds) && seconds >= 0 ? seconds : throw new CliUsageException(key); void Write(string sql) { if (Value("--output") is { } path) File.WriteAllText(path, sql); else output.WriteLine(sql); } } finally { AssemblyLoadContext.Default.Resolving -= Resolving; } diff --git a/src/Migrator/DatabaseMigrationLock.cs b/src/Migrator/DatabaseMigrationLock.cs index d2ae7bd3..d6e35d90 100644 --- a/src/Migrator/DatabaseMigrationLock.cs +++ b/src/Migrator/DatabaseMigrationLock.cs @@ -21,7 +21,7 @@ public IDisposable Acquire(ITransformationProvider provider, string scope, TimeS var kind = provider.Dialect switch { SqlServerDialect => 0, PostgreSQLDialect => 1, MysqlDialect => 2, - _ => throw new NotSupportedException("Database migration locking is supported on SQL Server, PostgreSQL and MySQL/MariaDB.") + _ => throw new UnsupportedMigrationFeatureException("Database migration locking is supported on SQL Server, PostgreSQL and MySQL/MariaDB.") }; var connection = provider.Connection; if (connection.State != ConnectionState.Open) throw new MigrationException("Migration locking requires an open connection."); diff --git a/src/Migrator/MigrationSqlPreview.cs b/src/Migrator/MigrationSqlPreview.cs index cf32793b..46cfbc37 100644 --- a/src/Migrator/MigrationSqlPreview.cs +++ b/src/Migrator/MigrationSqlPreview.cs @@ -17,6 +17,10 @@ public static string Generate(ProviderTypes provider, IEnumerable<(IMigration Mi var sql = new List(); foreach (var (migration, up) in migrations) { + var initialization = migration.GetType().GetInterfaceMap(typeof(IMigration)); + var initializeIndex = Array.FindIndex(initialization.InterfaceMethods, m => m.Name == nameof(IMigration.InitializeOnce)); + if (initialization.TargetMethods[initializeIndex].DeclaringType != typeof(Migration)) + throw new UnsupportedMigrationFeatureException("Preview rejects migrations with an InitializeOnce hook because executing initialization would violate read-only preview semantics."); var original = migration.Database; var proxy = DispatchProxy.Create(); var recorder = (PreviewProvider)(object)proxy; @@ -27,11 +31,15 @@ public static string Generate(ProviderTypes provider, IEnumerable<(IMigration Mi if (migration is FluentMigration fluent) operations = fluent.GetOperations(up); else { - if (!allowLegacyBodies) throw new NotSupportedException("Imperative SQL preview requires explicit allowLegacyBodies opt-in. Arbitrary C# cannot be sandboxed."); + if (!allowLegacyBodies) throw new UnsupportedMigrationFeatureException("Imperative SQL preview requires explicit allowLegacyBodies opt-in. Arbitrary C# cannot be sandboxed."); if (up) migration.Up(); else migration.Down(); operations = recorder.Operations; } - foreach (var operation in operations) sql.Add(operation.ToSql(context)); + foreach (var operation in operations) + { + try { sql.Add(operation.ToSql(context)); } + catch (NotSupportedException ex) { throw new UnsupportedMigrationFeatureException("This operation cannot be previewed.", ex); } + } } finally { migration.Database = original; } } @@ -53,7 +61,7 @@ protected override object Invoke(MethodInfo method, object[] args) "RenameColumn" => new RenameOperation((string)args[0], (string)args[2], (string)args[1]), "Insert" when args.Length == 3 && args[1] is string[] columns && args[2] is object[] values => new DataOperation(DataKind.Insert, (string)args[0], (string[])columns.Clone(), (object[])values.Clone()), "ExecuteNonQuery" when args.Length == 1 => new SqlOperation((string)args[0]), - _ => throw new NotSupportedException("SQL preview blocks provider member " + method.Name + ". Use a structured operation or an explicit SQL script.") + _ => throw new UnsupportedMigrationFeatureException("SQL preview blocks provider member " + method.Name + ". Use a structured operation or an explicit SQL script.") }; Operations.Add(operation); return method.ReturnType == typeof(int) ? 0 : null; diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index 1c52760a..d0822786 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -260,11 +260,17 @@ public void MigrateTo(long version) if (Options.LockTimeout < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(Options.LockTimeout)); var session = Options.TransactionMode == MigrationTransactionMode.WholeSession; if (session && _provider.Dialect is not (Providers.Impl.SQLite.SQLiteDialect or Providers.Impl.PostgreSQL.PostgreSQLDialect or Providers.Impl.SqlServer.SqlServerDialect)) - throw new NotSupportedException("Whole-session transactions require a verified transactional DDL provider (SQLite, PostgreSQL or SQL Server)."); + throw new UnsupportedMigrationFeatureException("Whole-session transactions require a verified transactional DDL provider (SQLite, PostgreSQL or SQL Server)."); _migrationLoader.Activator = Options.Activator; - using var lease = Options.Lock?.Acquire(_provider, (_provider as IMigrationHistory)?.Scope, Options.LockTimeout); + IDisposable AcquireLock() + { + try { return Options.Lock?.Acquire(_provider, (_provider as IMigrationHistory)?.Scope, Options.LockTimeout); } + catch (TimeoutException ex) { throw new MigrationLockTimeoutException(ex); } + } + using var lease = AcquireLock(); (_provider as IMigrationHistory)?.InvalidateHistory(); var history = new List(_provider.AppliedMigrations); + var initialHistory = new List(history); var plan = CreatePlan(history, version); var profiles = _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } p && Options.Profiles.Contains(p.Name) && _migrationLoader.InScope(p.Scope)) .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal).ToArray(); @@ -299,11 +305,11 @@ void Run() foreach (var type in profiles) Execute(_migrationLoader.CreateInstance(type), new MigrationStep(0, true), false); Maintenance(MaintenanceStage.AfterRun); } - Logger.Started(history, version); + Logger.Started(new List(initialHistory), version); if (session) MigrationExecution.InTransaction(_provider, true, Run); else Run(); foreach (var callback in afterCommit) callback(); history.Sort(); - Logger.Finished(history, version); + Logger.Finished(new List(initialHistory), version); } } diff --git a/src/Migrator/RunnerOptions.cs b/src/Migrator/RunnerOptions.cs index b473ea5a..0666f37d 100644 --- a/src/Migrator/RunnerOptions.cs +++ b/src/Migrator/RunnerOptions.cs @@ -33,3 +33,12 @@ public interface IMigrationLock { IDisposable Acquire(ITransformationProvider provider, string scope, TimeSpan timeout); } + +public sealed class UnsupportedMigrationFeatureException : NotSupportedException +{ + public UnsupportedMigrationFeatureException(string message, Exception inner = null) : base(message, inner) { } +} +public sealed class MigrationLockTimeoutException : TimeoutException +{ + public MigrationLockTimeoutException(Exception inner) : base("Timed out acquiring the migration lock.", inner) { } +} From 90d81cae870358aa7fc285bda990f23a963b8bbf Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:39:46 +0200 Subject: [PATCH 05/17] Preserve migration failures when releasing a deployment lock fails Release the lock in a finally block and retain a secondary release exception in the original failure Data. A release failure after successful execution still propagates. Add a regression covering a failed migration and failed lease disposal together. Validation: solution build; Unit 89 passed; SQLite 172 passed. --- src/Migrator.Tests/RunnerFeatureTests.cs | 14 ++++ src/Migrator/Migrator.cs | 88 ++++++++++++++---------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/Migrator.Tests/RunnerFeatureTests.cs b/src/Migrator.Tests/RunnerFeatureTests.cs index d31c4620..cd7889b6 100644 --- a/src/Migrator.Tests/RunnerFeatureTests.cs +++ b/src/Migrator.Tests/RunnerFeatureTests.cs @@ -150,6 +150,20 @@ [Test] public void LifecycleLogArgumentsRemainInitialHistorySnapshots() Assert.That(started, Is.Empty); Assert.That(finished, Is.Empty); Assert.That(p.AppliedMigrations, Is.EqualTo(new long[] { 1 })); } + [Test] public void LockReleaseFailureDoesNotMaskMigrationFailure() + { + using var p = Provider(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(Failure)); + runner.Options.Lock = new FailingReleaseLock(); + var error = Assert.Throws(() => runner.MigrateToLastVersion()); + Assert.That(error.Message, Is.EqualTo("migration failed")); + Assert.That(error.Data["LockReleaseException"], Is.TypeOf()); + } + private sealed class FailingReleaseLock : IMigrationLock, IDisposable + { + public IDisposable Acquire(ITransformationProvider p, string scope, TimeSpan timeout) => this; + public void Dispose() => throw new ApplicationException("release failed"); + } private sealed class ProbeLock : IMigrationLock, IDisposable { public bool Disposed { get; private set; } diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index d0822786..620fdd18 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -267,49 +267,63 @@ IDisposable AcquireLock() try { return Options.Lock?.Acquire(_provider, (_provider as IMigrationHistory)?.Scope, Options.LockTimeout); } catch (TimeoutException ex) { throw new MigrationLockTimeoutException(ex); } } - using var lease = AcquireLock(); - (_provider as IMigrationHistory)?.InvalidateHistory(); - var history = new List(_provider.AppliedMigrations); - var initialHistory = new List(history); - var plan = CreatePlan(history, version); - var profiles = _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } p && Options.Profiles.Contains(p.Name) && _migrationLoader.InScope(p.Scope)) - .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal).ToArray(); - foreach (var name in Options.Profiles) - if (!profiles.Any(t => t.GetCustomAttribute().Name == name)) throw new MigrationException("Unknown profile: " + name); - var afterCommit = new List(); - var firstRun = true; - void Execute(IMigration migration, MigrationStep step, bool record) - { - migration.Database = _provider; - if (firstRun) { migration.InitializeOnce(_args); firstRun = false; } - MigrationExecution.Execute(_provider, migration, step, Logger, - Options.TransactionMode == MigrationTransactionMode.PerMigration, session, record, !session); - if (session) afterCommit.Add(() => MigrationExecution.After(migration, step.IsUp)); - } - void Maintenance(MaintenanceStage stage) + var lease = AcquireLock(); + Exception failure = null; + try { - foreach (var type in _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } a && a.Stage == stage && _migrationLoader.InScope(a.Scope)) - .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal)) - Execute(_migrationLoader.CreateInstance(type), new MigrationStep(0, true), false); + (_provider as IMigrationHistory)?.InvalidateHistory(); + var history = new List(_provider.AppliedMigrations); + var initialHistory = new List(history); + var plan = CreatePlan(history, version); + var profiles = _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } p && Options.Profiles.Contains(p.Name) && _migrationLoader.InScope(p.Scope)) + .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal).ToArray(); + foreach (var name in Options.Profiles) + if (!profiles.Any(t => t.GetCustomAttribute().Name == name)) throw new MigrationException("Unknown profile: " + name); + var afterCommit = new List(); + var firstRun = true; + void Execute(IMigration migration, MigrationStep step, bool record) + { + migration.Database = _provider; + if (firstRun) { migration.InitializeOnce(_args); firstRun = false; } + MigrationExecution.Execute(_provider, migration, step, Logger, + Options.TransactionMode == MigrationTransactionMode.PerMigration, session, record, !session); + if (session) afterCommit.Add(() => MigrationExecution.After(migration, step.IsUp)); + } + void Maintenance(MaintenanceStage stage) + { + foreach (var type in _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } a && a.Stage == stage && _migrationLoader.InScope(a.Scope)) + .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal)) + Execute(_migrationLoader.CreateInstance(type), new MigrationStep(0, true), false); + } + void Run() + { + Maintenance(MaintenanceStage.BeforeRun); + foreach (var step in plan) + { + Maintenance(MaintenanceStage.BeforeMigration); + Execute(_migrationLoader.GetMigration(step.Version), step, true); + if (step.IsUp) history.Add(step.Version); else history.Remove(step.Version); + Maintenance(MaintenanceStage.AfterMigration); + } + foreach (var type in profiles) Execute(_migrationLoader.CreateInstance(type), new MigrationStep(0, true), false); + Maintenance(MaintenanceStage.AfterRun); + } + Logger.Started(new List(initialHistory), version); + if (session) MigrationExecution.InTransaction(_provider, true, Run); else Run(); + foreach (var callback in afterCommit) callback(); + history.Sort(); + Logger.Finished(new List(initialHistory), version); } - void Run() + catch (Exception ex) { failure = ex; throw; } + finally { - Maintenance(MaintenanceStage.BeforeRun); - foreach (var step in plan) + try { lease?.Dispose(); } + catch (Exception release) { - Maintenance(MaintenanceStage.BeforeMigration); - Execute(_migrationLoader.GetMigration(step.Version), step, true); - if (step.IsUp) history.Add(step.Version); else history.Remove(step.Version); - Maintenance(MaintenanceStage.AfterMigration); + if (failure == null) throw; + failure.Data["LockReleaseException"] = release; } - foreach (var type in profiles) Execute(_migrationLoader.CreateInstance(type), new MigrationStep(0, true), false); - Maintenance(MaintenanceStage.AfterRun); } - Logger.Started(new List(initialHistory), version); - if (session) MigrationExecution.InTransaction(_provider, true, Run); else Run(); - foreach (var callback in afterCommit) callback(); - history.Sort(); - Logger.Finished(new List(initialHistory), version); } } From 3747c8d58a4dae2aed98dafeed1c8a4a4f5b6244 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:41:25 +0200 Subject: [PATCH 06/17] Make CLI rollback reject upward migration targets under the lock Add an additive RollbackTo entry point that validates the refreshed execution plan after acquiring the deployment lock. Route the CLI rollback command through it so an empty database and a higher target cannot execute Up migrations. Validation: solution build, Unit 89 passed, SQLite 173 passed; the new connected CLI regression verifies no user table or version is applied. Addresses review 4072146578. --- src/Migrator.Tests/ToolingTests.cs | 19 +++++++++++++++++++ src/Migrator.Tool/Program.cs | 1 + src/Migrator/Migrator.cs | 13 +++++++++++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Migrator.Tests/ToolingTests.cs b/src/Migrator.Tests/ToolingTests.cs index b1da945b..7a81d65d 100644 --- a/src/Migrator.Tests/ToolingTests.cs +++ b/src/Migrator.Tests/ToolingTests.cs @@ -70,6 +70,25 @@ public void CliMigratesReadsStatusAndRollsBackWithPackagedDriver() } finally { Environment.SetEnvironmentVariable(environmentName, null); File.Delete(file); } } + [Test, Category("SQLite")] + public void RollbackCommandRejectsAnUpwardTargetWithoutCreatingUserTables() + { + var file = Path.Combine(Path.GetTempPath(), "migrator-rollback-" + Guid.NewGuid().ToString("N") + ".db"); + var variable = "MIGRATOR_TEST_" + Guid.NewGuid().ToString("N"); + Environment.SetEnvironmentVariable(variable, "Data Source=" + file + ";Pooling=False"); + try + { + using var output = new StringWriter(); using var error = new StringWriter(); + Assert.That(MigratorCommand.Run(new[] { "rollback", "--assembly", typeof(ToolingTests).Assembly.Location, + "--provider", "SQLite", "--scope", "cli-spec", "--connection-env", variable, + "--target", "900002" }, output, error), Is.EqualTo(1)); + using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=" + file + ";Pooling=False"); connection.Open(); + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null, "cli-spec"); + Assert.That(provider.TableExists("CliExample"), Is.False); + Assert.That(((IMigrationHistory)provider).ReadAppliedMigrations(), Is.Empty); + } + finally { Environment.SetEnvironmentVariable(variable, null); File.Delete(file); } + } [Migration(900003, Scope = "cli-errors")] public class FailingCliMigration : Migration { diff --git a/src/Migrator.Tool/Program.cs b/src/Migrator.Tool/Program.cs index a99c177a..93701fe1 100644 --- a/src/Migrator.Tool/Program.cs +++ b/src/Migrator.Tool/Program.cs @@ -115,6 +115,7 @@ bool Selected(Type t) case "validate": _ = runner.Plan(target); output.WriteLine("Migration plan is valid."); break; case "plan": foreach (var step in runner.Plan(target)) output.WriteLine(step.Version + (step.IsUp ? " up" : " down")); break; case "sql": Write(runner.PreviewSql(target, providerType, values.ContainsKey("--allow-legacy-preview"))); break; + case "rollback": runner.RollbackTo(target); output.WriteLine("Rollback completed."); break; default: runner.MigrateTo(target); output.WriteLine("Migration completed."); break; } return 0; diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index 620fdd18..1c854002 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -249,11 +249,18 @@ void AddMaintenance(MaintenanceStage stage) table => _provider.TableExists(table) ? _provider.GetColumns(table) : throw new MigrationException("Preview table does not exist: " + table)); } - public void MigrateTo(long version) + public void MigrateTo(long version) => MigrateTo(version, false); + + /// Run only downward steps; validate the target after acquiring the configured lock. + public void RollbackTo(long version) => MigrateTo(version, true); + + private void MigrateTo(long version, bool downOnly) { if (DryRun) { - foreach (var step in Plan(version)) + var preview = Plan(version); + if (downOnly && preview.Any(step => step.IsUp)) throw new MigrationException("Rollback cannot apply upward migrations."); + foreach (var step in preview) if (step.IsUp) Logger.MigrateUp(step.Version, "Preview"); else Logger.MigrateDown(step.Version, "Preview"); return; } @@ -275,6 +282,8 @@ IDisposable AcquireLock() var history = new List(_provider.AppliedMigrations); var initialHistory = new List(history); var plan = CreatePlan(history, version); + if (downOnly && (version >= history.DefaultIfEmpty(0).Max() || plan.Any(step => step.IsUp))) + throw new MigrationException("Rollback requires a lower target and cannot apply upward migrations."); var profiles = _migrationLoader.AuxiliaryTypes.Where(t => t.GetCustomAttribute() is { } p && Options.Profiles.Contains(p.Name) && _migrationLoader.InScope(p.Scope)) .OrderBy(t => t.GetCustomAttribute().Order).ThenBy(t => t.FullName, StringComparer.Ordinal).ToArray(); foreach (var name in Options.Profiles) From bde0a054188a167b5f07159b0d9cda09841a31f1 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:59:53 +0200 Subject: [PATCH 07/17] Replace stale matrix artifacts when failed CI jobs are rerun The documentation PR reproduced an Informix registry timeout followed by a successful retry, but coverage downloaded an empty startup-only artifact left by the first attempt. Enable upload-artifact overwrite for each uniquely named database suite so the gate consumes the current attempt's test results. Validation: inspected run 35735397572 attempt 2: all database jobs passed, while duplicate test-results-Informix artifacts (277 and 16046 bytes) caused the missing-suite failure. Fresh CI validates the complete artifact flow. --- .github/workflows/dotnetpull.yml | 169 ++++++++++++++++--------------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 7dba5873..410f736a 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -1,84 +1,85 @@ -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 +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 }} + overwrite: true + 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 From b4a6edda8f8119e2ece297394e6af378959a8e41 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:21:23 +0200 Subject: [PATCH 08/17] Exercise concurrent runners and stale history under native deployment locks Add a two-connection runner regression on SQL Server, PostgreSQL, MySQL and MariaDB. Seed the second runner with stale empty history, hold the first inside its migration, start the second acquisition, and verify only one Up executes after history reload. Confirm the final version and that the lock can be acquired again. Coordination uses events instead of timing sleeps. Validation: solution build and Unit 96 passed. The new concurrency cases require their four live provider CI jobs before claiming coverage. --- src/Migrator.Tests/DatabaseLockTests.cs | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/Migrator.Tests/DatabaseLockTests.cs b/src/Migrator.Tests/DatabaseLockTests.cs index 834761a9..1d6018e0 100644 --- a/src/Migrator.Tests/DatabaseLockTests.cs +++ b/src/Migrator.Tests/DatabaseLockTests.cs @@ -1,5 +1,8 @@ using System; using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator; using DotNetProjects.Migrator.Providers; using Migrator.Tests.Settings; @@ -28,6 +31,64 @@ private DbConnection Open() ?? "Server=127.0.0.1;Database=testdb;User ID=root;Password=rootpass;Pooling=false"); connection.Open(); return connection; } + private sealed class RunState : IDisposable + { + public int Calls; + public readonly ManualResetEventSlim Entered = new(); + public readonly ManualResetEventSlim Release = new(); + public void Dispose() { Entered.Dispose(); Release.Dispose(); } + } + [Migration(1)] + private sealed class CountMigration(RunState state) : Migration + { + public override void Up() + { + Interlocked.Increment(ref state.Calls); + state.Entered.Set(); + if (!state.Release.Wait(TimeSpan.FromSeconds(20))) throw new TimeoutException("Test migration gate timed out."); + } + public override void Down() { } + } + private sealed class SignallingLock(ManualResetEventSlim attempted) : IMigrationLock + { + public IDisposable Acquire(ITransformationProvider provider, string scope, TimeSpan timeout) + { attempted.Set(); return new DatabaseMigrationLock().Acquire(provider, scope, timeout); } + } + [Test] + public async Task ConcurrentRunnersReloadStaleHistoryAfterAcquiringNativeLock() + { + using var connection1 = Open(); using var connection2 = Open(); + using var p1 = ProviderFactory.Create(type, connection1, null); + using var p2 = ProviderFactory.Create(type, connection2, null); + p1.SchemaInfoTable = p2.SchemaInfoTable = "lockhistory_" + Guid.NewGuid().ToString("N")[..12]; + using var state = new RunState(); using var attempted = new ManualResetEventSlim(); + Assert.That(p2.AppliedMigrations, Is.Empty); // Deliberately seed a stale empty cache. + var first = new DotNetProjects.Migrator.Migrator(p1, false, typeof(CountMigration)); + var second = new DotNetProjects.Migrator.Migrator(p2, false, typeof(CountMigration)); + first.Options.Activator = second.Options.Activator = _ => new CountMigration(state); + first.Options.Lock = new DatabaseMigrationLock(); second.Options.Lock = new SignallingLock(attempted); + first.Options.LockTimeout = second.Options.LockTimeout = TimeSpan.FromSeconds(15); + Task one = null, two = null; + try + { + one = Task.Run(first.MigrateToLastVersion); + Assert.That(state.Entered.Wait(TimeSpan.FromSeconds(10)), Is.True); + two = Task.Run(second.MigrateToLastVersion); + Assert.That(attempted.Wait(TimeSpan.FromSeconds(10)), Is.True); + state.Release.Set(); + await Task.WhenAll(one, two); + Assert.That(state.Calls, Is.EqualTo(1)); + Assert.That(((IMigrationHistory)p2).ReadAppliedMigrations(), Is.EqualTo(new long[] { 1 })); + using var released = new DatabaseMigrationLock().Acquire(p2, ((IMigrationHistory)p2).Scope, TimeSpan.Zero); + } + finally + { + state.Release.Set(); + try { if (one != null) await one; if (two != null) await two; } + finally { p1.RemoveTable(p1.SchemaInfoTable); } + } + } + [Test] public void IndependentSessionsContendAndCanAcquireAfterRelease() { using var connection1 = Open(); using var connection2 = Open(); From 5203ae28b847dac4cc3c4575293a187d7874d155 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:22:15 +0200 Subject: [PATCH 09/17] Await both concurrent runner tasks before test database cleanup Even when one worker faults, await the remaining worker before dropping the temporary history table or disposing its connection. This keeps the failure path of the concurrency regression deterministic. --- src/Migrator.Tests/DatabaseLockTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Migrator.Tests/DatabaseLockTests.cs b/src/Migrator.Tests/DatabaseLockTests.cs index 1d6018e0..5a72fc55 100644 --- a/src/Migrator.Tests/DatabaseLockTests.cs +++ b/src/Migrator.Tests/DatabaseLockTests.cs @@ -1,5 +1,6 @@ using System; using System.Data.Common; +using System.Linq; using System.Threading; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; @@ -84,7 +85,7 @@ public async Task ConcurrentRunnersReloadStaleHistoryAfterAcquiringNativeLock() finally { state.Release.Set(); - try { if (one != null) await one; if (two != null) await two; } + try { await Task.WhenAll(new[] { one, two }.Where(task => task != null)); } finally { p1.RemoveTable(p1.SchemaInfoTable); } } } From 9804fef63c95311992451fd3ffa6db74cda6e983 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:42:32 +0200 Subject: [PATCH 10/17] Preserve runner review fixes while aligning the PR stack with master Retain the already-validated callback CurrentMigration context, auxiliary-only history preservation and regression tests when replaying the tooling branch on the updated runner/provider bases. These fixes had equivalent patches earlier in the old stack; rebase patch deduplication otherwise omitted their final tooling integration. Preserve the original file encodings and matrix settings. The resulting source tree matches the previously tested bc35e0e exactly; only the comparison and homepage additions already merged on master are new here. --- .github/workflows/dotnetpull.yml | 170 ++++++------ src/Migrator.Tests/Migrator.Tests.csproj | 2 +- src/Migrator.Tests/RunnerFeatureTests.cs | 19 +- src/Migrator/MigrationExecution.cs | 14 +- src/Migrator/MigrationLoader.cs | 332 +++++++++++------------ src/Migrator/Migrator.cs | 16 +- 6 files changed, 292 insertions(+), 261 deletions(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 410f736a..52020f5b 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -1,85 +1,85 @@ -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 }} - overwrite: true - 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 }} + overwrite: true + 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/Migrator.Tests.csproj b/src/Migrator.Tests/Migrator.Tests.csproj index c6c1926f..061f4d0f 100644 --- a/src/Migrator.Tests/Migrator.Tests.csproj +++ b/src/Migrator.Tests/Migrator.Tests.csproj @@ -50,8 +50,8 @@ - + diff --git a/src/Migrator.Tests/RunnerFeatureTests.cs b/src/Migrator.Tests/RunnerFeatureTests.cs index cd7889b6..66bff1cd 100644 --- a/src/Migrator.Tests/RunnerFeatureTests.cs +++ b/src/Migrator.Tests/RunnerFeatureTests.cs @@ -17,7 +17,12 @@ internal class First : Migration { public override void Up() { Events.Add("first"); Database.AddTable("First", new Column("Id", DbType.Int32)); } public override void Down() => Database.RemoveTable("First"); - public override void AfterUp() => Events.Add("committed"); + public override void AfterUp() + { + Assert.That(((TransformationProvider)Database).CurrentMigration, Is.SameAs(this)); + Assert.That(((TransformationProvider)Database).HasActiveTransaction, Is.False); + Events.Add("committed"); + } } [Migration(2), Tags("red", "shared")] internal class Second : Migration @@ -61,6 +66,18 @@ [Test] public void ProfilesAndMaintenanceHaveDeterministicOrderAndNoHistory() Assert.That(p.AppliedMigrations, Is.EqualTo(new long[] { 1 })); Assert.That(Convert.ToInt64(p.ExecuteScalar("SELECT Id FROM First")), Is.EqualTo(7)); } + [Test] public void AuxiliaryOnlyLatestRunPreservesExistingVersions() + { + using var p = Provider(); + new DotNetProjects.Migrator.Migrator(p, false, typeof(First)).MigrateToLastVersion(); + Events.Clear(); + var runner = new DotNetProjects.Migrator.Migrator(p, false, typeof(Before), typeof(Seed), typeof(After)); + runner.Options.Profiles.Add("seed"); + runner.MigrateToLastVersion(); + Assert.That(Events, Is.EqualTo(new[] { "before", "profile", "after" })); + Assert.That(p.AppliedMigrations, Is.EqualTo(new long[] { 1 })); + Assert.That(Convert.ToInt64(p.ExecuteScalar("SELECT Id FROM First")), Is.EqualTo(7)); + } [TestCase(TagMatchMode.Any, 2)] [TestCase(TagMatchMode.All, 1)] public void TagsUseExplicitAnyOrAll(TagMatchMode mode, int expected) diff --git a/src/Migrator/MigrationExecution.cs b/src/Migrator/MigrationExecution.cs index 58e6b1eb..9a50aca3 100644 --- a/src/Migrator/MigrationExecution.cs +++ b/src/Migrator/MigrationExecution.cs @@ -32,12 +32,9 @@ void Body() catch (Exception ex) { logger.Exception(step.Version, migration.Name, ex); throw; } finally { if (concrete != null) concrete.CurrentMigration = null; } // Session callbacks are deferred until the outer transaction commits. - if (callbacks) After(migration, step.IsUp); + if (callbacks) After(provider, migration, step.IsUp); } - internal static void After(IMigration migration, bool up) - { if (up) migration.AfterUp(); else migration.AfterDown(); } - internal static void InTransaction(ITransformationProvider provider, bool transaction, Action body) { if ((provider as TransformationProvider)?.HasActiveTransaction == true) @@ -74,4 +71,13 @@ internal static void InTransaction(ITransformationProvider provider, bool transa (provider as IMigrationHistory)?.InvalidateHistory(); } } + 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/MigrationLoader.cs b/src/Migrator/MigrationLoader.cs index 70a220e7..932164b3 100644 --- a/src/Migrator/MigrationLoader.cs +++ b/src/Migrator/MigrationLoader.cs @@ -1,166 +1,166 @@ -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}", (t.GetCustomAttribute()?.Version.ToString() ?? "aux").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}", (t.GetCustomAttribute()?.Version.ToString() ?? "aux").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 Func Activator { get; set; } - - public IEnumerable SelectedTypes => _migrationsTypes.Where(t => - t.GetCustomAttribute() != null && InScope(t.GetCustomAttribute().Scope)); - - internal bool InScope(string scope) => scope == null || _provider is not IMigrationHistory history || scope == history.Scope; - internal IEnumerable AuxiliaryTypes => _migrationsTypes.Where(t => t.GetCustomAttribute() == null); - - - 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 (t.IsAbstract || !typeof(IMigration).IsAssignableFrom(t)) continue; - var versioned = t.GetCustomAttribute(); - if (versioned != null ? !versioned.Ignore : - t.GetCustomAttribute() != null || t.GetCustomAttribute() != null) - migrations.Add(t); - } - migrations = migrations.OrderBy(t => t.GetCustomAttribute()?.Version ?? 0).ThenBy(t => t.FullName, StringComparer.Ordinal).ToList(); - 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() - { - return SelectedTypes.Select(GetMigrationVersion).OrderBy(v => v).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 Activator != null ? Activator(migrationType) ?? throw new MigrationException("Migration activator returned null.") : (IMigration)System.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}", (t.GetCustomAttribute()?.Version.ToString() ?? "aux").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}", (t.GetCustomAttribute()?.Version.ToString() ?? "aux").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 Func Activator { get; set; } + + public IEnumerable SelectedTypes => _migrationsTypes.Where(t => + t.GetCustomAttribute() != null && InScope(t.GetCustomAttribute().Scope)); + + internal bool InScope(string scope) => scope == null || _provider is not IMigrationHistory history || scope == history.Scope; + internal IEnumerable AuxiliaryTypes => _migrationsTypes.Where(t => t.GetCustomAttribute() == null); + + + 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 (t.IsAbstract || !typeof(IMigration).IsAssignableFrom(t)) continue; + var versioned = t.GetCustomAttribute(); + if (versioned != null ? !versioned.Ignore : + t.GetCustomAttribute() != null || t.GetCustomAttribute() != null) + migrations.Add(t); + } + migrations = migrations.OrderBy(t => t.GetCustomAttribute()?.Version ?? 0).ThenBy(t => t.FullName, StringComparer.Ordinal).ToList(); + 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() + { + return SelectedTypes.Select(GetMigrationVersion).OrderBy(v => v).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 Activator != null ? Activator(migrationType) ?? throw new MigrationException("Migration activator returned null.") : (IMigration)System.Activator.CreateInstance(migrationType); + } +} diff --git a/src/Migrator/Migrator.cs b/src/Migrator/Migrator.cs index 1c854002..9d94f03c 100644 --- a/src/Migrator/Migrator.cs +++ b/src/Migrator/Migrator.cs @@ -183,7 +183,14 @@ public long? LastAppliedMigrationVersion /// public void MigrateToLastVersion() { - MigrateTo(SelectedMigrationTypes.Select(MigrationLoader.GetMigrationVersion).DefaultIfEmpty(0).Max()); + var versions = SelectedMigrationTypes.Select(MigrationLoader.GetMigrationVersion).ToArray(); + if (versions.Length == 0 && Options.Profiles.Count == 0 && + !_migrationLoader.AuxiliaryTypes.Any(t => t.GetCustomAttribute() is { } a && _migrationLoader.InScope(a.Scope))) + { + Logger.Warn("No migrations found for the effective scope."); + return; + } + MigrateTo(versions.DefaultIfEmpty(0).Max(), false, versions.Length == 0); } /// @@ -254,11 +261,11 @@ void AddMaintenance(MaintenanceStage stage) /// Run only downward steps; validate the target after acquiring the configured lock. public void RollbackTo(long version) => MigrateTo(version, true); - private void MigrateTo(long version, bool downOnly) + private void MigrateTo(long version, bool downOnly, bool preserveVersion = false) { if (DryRun) { - var preview = Plan(version); + var preview = preserveVersion ? Array.Empty() : Plan(version); if (downOnly && preview.Any(step => step.IsUp)) throw new MigrationException("Rollback cannot apply upward migrations."); foreach (var step in preview) if (step.IsUp) Logger.MigrateUp(step.Version, "Preview"); else Logger.MigrateDown(step.Version, "Preview"); @@ -281,6 +288,7 @@ IDisposable AcquireLock() (_provider as IMigrationHistory)?.InvalidateHistory(); var history = new List(_provider.AppliedMigrations); var initialHistory = new List(history); + if (preserveVersion) version = history.DefaultIfEmpty(0).Max(); var plan = CreatePlan(history, version); if (downOnly && (version >= history.DefaultIfEmpty(0).Max() || plan.Any(step => step.IsUp))) throw new MigrationException("Rollback requires a lower target and cannot apply upward migrations."); @@ -296,7 +304,7 @@ void Execute(IMigration migration, MigrationStep step, bool record) if (firstRun) { migration.InitializeOnce(_args); firstRun = false; } MigrationExecution.Execute(_provider, migration, step, Logger, Options.TransactionMode == MigrationTransactionMode.PerMigration, session, record, !session); - if (session) afterCommit.Add(() => MigrationExecution.After(migration, step.IsUp)); + if (session) afterCommit.Add(() => MigrationExecution.After(_provider, migration, step.IsUp)); } void Maintenance(MaintenanceStage stage) { From 8cde651a6a30b91e8e8e36655ff6060fff416649 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:13:13 +0200 Subject: [PATCH 11/17] Document upgrade source capabilities and refresh framework comparison Update README, static homepage and the comparison newly merged to master with effective scopes, fluent operations, preview limitations, transaction modes, profiles, maintenance, locking, CLI and optional DI/logging. Pin source evidence and distinguish under-review source features from released packages. Add a compiled quick-start that verifies read-only preview, migration and automatic reversal. Validation: compiled and ran the SQLite quick-start; packed and locally installed the tool; offline SQL generation succeeded. Inspected homepage at desktop/mobile widths. Connected CLI smoke exposed a missing driver-factory registration and is being corrected in the tooling PR before docs completion. --- .gitignore | 39 +- Migrator.slnx | 3 + README.md | 624 ++++---- docs/index.html | 1403 +++++++++-------- docs/migration-framework-comparison.md | 826 +++++----- docs/runner-guide.md | 100 ++ .../FluentQuickStart/FluentQuickStart.csproj | 4 + examples/FluentQuickStart/Program.cs | 30 + 8 files changed, 1588 insertions(+), 1441 deletions(-) create mode 100644 docs/runner-guide.md create mode 100644 examples/FluentQuickStart/FluentQuickStart.csproj create mode 100644 examples/FluentQuickStart/Program.cs diff --git a/.gitignore b/.gitignore index c92f3f9b..02f80861 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,20 @@ -bin/ -obj/ -*.log -logs/ -_ReSharper*/ -output/ -release/ -*.suo -*.user -*.cache -packages/ - -.vs/ - -/src/GlobalAssemblyInfo.cs -*.gpState - -**/appsettings.Development.json -TestResults/ +bin/ +obj/ +*.log +logs/ +_ReSharper*/ +output/ +release/ +*.suo +*.user +*.cache +packages/ + +.vs/ + +/src/GlobalAssemblyInfo.cs +*.gpState + +**/appsettings.Development.json +TestResults/ +/artifacts/ diff --git a/Migrator.slnx b/Migrator.slnx index cf4bc016..0fad1ce6 100644 --- a/Migrator.slnx +++ b/Migrator.slnx @@ -2,6 +2,9 @@ + + + diff --git a/README.md b/README.md index e8f8b420..529ea7a0 100644 --- a/README.md +++ b/README.md @@ -1,307 +1,317 @@ -# DotNetProjects.Migrator - -**Versioned database migrations in C#, independent of your ORM.** - -[![NuGet version](https://img.shields.io/nuget/v/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) -[![NuGet downloads](https://img.shields.io/nuget/dt/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) -[![Build and tests](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml) -[![GitHub Pages](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml) -[![Source target: .NET 9](https://img.shields.io/badge/source_target-.NET_9-512BD4)](src/Migrator/DotNetProjects.Migrator.csproj) -[![License: MPL-1.1](https://img.shields.io/badge/license-MPL--1.1-blue.svg)](https://www.mozilla.org/en-US/MPL/1.1/) - -[Homepage & documentation](https://dotnetprojects.github.io/Migrator.NET/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) - -DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratordotnet/Migrator.NET). Write each schema change as a numbered C# class, commit it alongside your application, and use the runner to bring a database to the required version. The database records which migrations have already been applied. - -## Contents - -- [Why use it?](#why-use-it) -- [Installation and requirements](#installation-and-requirements) -- [Quick start](#quick-start) -- [Migration versions and rollback](#migration-versions-and-rollback) -- [Multiple modules and migration scopes](#multiple-modules-and-migration-scopes) -- [Schema and data operations](#schema-and-data-operations) -- [Database providers](#database-providers) -- [Comparison with other .NET frameworks](#comparison-with-other-net-frameworks) -- [Building and testing](#building-and-testing) -- [Documentation and GitHub Pages](#documentation-and-github-pages) -- [Contributing and project history](#contributing-and-project-history) -- [License](#license) - -## Why use it? - -- **Explicit C# migrations.** Define forward and reverse changes with `Up()` and `Down()`; review them like application code. -- **No ORM dependency.** Use it alongside EF, Dapper, another data layer, or plain ADO.NET. -- **Database transformation API.** Work with tables, columns, keys, indexes and data, with raw SQL available for provider-specific operations. -- **Version tracking.** Apply pending migrations or target a specific version using database-backed history. -- **Scoped histories.** Track multiple modules in one database when each runner is given the appropriate migration set. -- **Bring your database driver.** The library does not directly reference database-driver packages; supply an ADO.NET connection or configure the driver factory. -- **SQLite schema handling.** This fork includes schema inspection and table-recreation logic for operations SQLite cannot perform directly. - -Migrator is a library you embed in a migration host. It does not provide EF-style model-difference scaffolding, a packaged command-line runner, or built-in migration-content checksum validation. - -## Installation and requirements - -```sh -dotnet add package DotNetProjects.Migrator -``` - -Install the ADO.NET driver for your database separately. For the SQLite example below: - -```sh -dotnet add package Microsoft.Data.Sqlite --version 9.0.7 -``` - -The **current source targets `net9.0`**. Check the [NuGet package's framework list](https://www.nuget.org/packages/DotNetProjects.Migrator/#supportedframeworks-body-tab) for the particular release you install; older package releases may target different frameworks. The SQLite driver version above matches the repository's test dependency. - -Building the `.slnx` solution requires an SDK that understands that format, such as .NET SDK 9.0.200 or later. The runtime required by the current source is .NET 9. - -## Quick start - -### 1. Create a migration host - -```sh -dotnet new console -n MigrationDemo -f net9.0 -cd MigrationDemo -dotnet add package DotNetProjects.Migrator -dotnet add package Microsoft.Data.Sqlite --version 9.0.7 -``` - -### 2. Add `CreateUsers.cs` - -Migrations must be public classes implementing the migration contract, decorated with `[Migration(version)]`. Each version must be unique within the set loaded by one runner. - -```csharp -using System.Data; -using DotNetProjects.Migrator.Framework; - -[Migration(1)] -public class CreateUsers : Migration -{ - public override void Up() - { - Database.AddTable("Users", - new Column("Id", DbType.Int32, ColumnProperty.NotNull), - new Column("Name", DbType.String, 255)); - Database.AddPrimaryKey("PK_Users", "Users", "Id"); - } - - public override void Down() - { - Database.RemoveTable("Users"); - } -} -``` - -### 3. Replace `Program.cs` - -```csharp -using DotNetProjects.Migrator; -using DotNetProjects.Migrator.Providers; -using Microsoft.Data.Sqlite; - -using var connection = new SqliteConnection("Data Source=app.db"); -connection.Open(); - -using var provider = ProviderFactory.Create( - ProviderTypes.SQLite, connection, defaultSchema: null); - -var migrator = new Migrator( - provider, typeof(CreateUsers).Assembly, trace: false); - -if (migrator.LastAppliedMigrationVersion is long applied - && applied > migrator.AssemblyLastMigrationVersion) -{ - throw new InvalidOperationException( - "Database version is newer than this application."); -} - -migrator.MigrateToLastVersion(); -``` - -### 4. Run it - -```sh -dotnet run -``` - -This creates a local SQLite database containing `Users` and the migration history table. Running the application again skips version `1` because it has already been recorded. Add a new class with `[Migration(2)]` for the next change. - -The example supplies an **open** `IDbConnection`. The caller owns that connection and disposes it after the provider. If you use the connection-string overload instead, the selected provider must be able to resolve the appropriate ADO.NET factory. - -## Migration versions and rollback - -Use increasing numeric versions, or the attribute's date-based constructor: - -```csharp -[Migration(2026, 9, 22, 12, 0, 0)] -``` - -Keep applied migration classes in source control. Change the schema with a new migration instead of editing an already applied one: history records the version, not a checksum of the migration's content. - -| API | Purpose | -| ------------------------------ | -------------------------------------------------------------------------------- | -| `MigrateToLastVersion()` | Apply through the latest version in the loaded migration set. | -| `MigrateTo(version)` | Move to a chosen version, invoking `Up()` or `Down()` as required. | -| `AppliedMigrations` | List the versions recorded for the provider's scope. | -| `LastAppliedMigrationVersion` | Highest applied version, or `null` when none are applied. | -| `AssemblyLastMigrationVersion` | Highest version in the loaded migration set. | -| `SchemaInfoTableName` | Customize the history table name before accessing history or running migrations. | - -With the runner above, `migrator.MigrateTo(0)` reverses all applied migrations in its set. In this example that drops `Users`, including its data. A `Down()` implementation is a reverse schema operation, not a backup restore. - -Migration execution starts a transaction for each migration and attempts rollback on failure. Actual atomicity depends on the database, driver and operation; some databases implicitly commit DDL. `AfterUp()` and `AfterDown()` run **after commit**, so a failure in those hooks cannot undo the committed migration. - -For deployment, run a dedicated migration host before the application needs the new schema. Coordinate it so competing instances do not migrate the same database concurrently. Review and test both directions against your actual database engine. - -## Multiple modules and migration scopes - -The default history table is `SchemaInfo`, with version, scope and timestamp information. The default scope is `"default"`. You can use separate scopes for modules sharing a database. - -Within a host with an open `connection`, select the module's migration types explicitly: - -```csharp -using var billingProvider = ProviderFactory.Create( - ProviderTypes.SQLite, - connection, - defaultSchema: null, - scope: "billing"); - -var billingMigrator = new Migrator( - billingProvider, - false, - typeof(Billing001), - typeof(Billing002)); - -billingMigrator.MigrateToLastVersion(); -``` - -`Billing001` and `Billing002` represent your own public migration classes. Alternatively, give the runner an assembly that contains only that module's migrations. - -Important details: - -- 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). - -## Schema and data operations - -Inside a migration, `Database` implements [`ITransformationProvider`](src/Migrator/Framework/ITransformationProvider.cs). It includes: - -| Area | Examples | -| ------------------ | ------------------------------------------------------------------------------------- | -| Tables and columns | `AddTable`, `RemoveTable`, `RenameTable`, `AddColumn`, `ChangeColumn`, `RemoveColumn` | -| Keys and indexes | `AddPrimaryKey`, `AddForeignKey`, `AddIndex` and corresponding removal operations | -| Schema inspection | `TableExists`, `ColumnExists`, `GetTables`, `GetColumns` | -| Data and SQL | `Insert`, `Update`, `Delete`, `ExecuteNonQuery`, `ExecuteQuery`, `ExecuteScalar` | - -For example, a new migration can add a column: - -```csharp -public override void Up() -{ - Database.AddColumn("Users", new Column("Email", DbType.String, 320)); -} - -public override void Down() -{ - Database.RemoveColumn("Users", "Email"); -} -``` - -Provider implementations determine which operations are available and how they map to SQL. Use `Database.ExecuteNonQuery(...)` for custom SQL and keep dialect-specific statements explicit. The source also includes a [schema builder API](src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs). - -## Database providers - -The [provider factory](src/Migrator/ProviderFactory.cs) contains these database families: - -| Database | `ProviderTypes` value(s) | -| ------------ | ---------------------------- | -| SQL Server | `SqlServer`, `SqlServer2005` | -| PostgreSQL | `PostgreSQL`, `PostgreSQL82` | -| SQLite | `SQLite`, `MonoSQLite` | -| MySQL | `Mysql` | -| MariaDB | `MariaDB` | -| Oracle | `Oracle`, `MsOracle` | -| IBM Db2 | `IBM_DB2` | -| IBM Informix | `IBM_Informix` | -| Firebird | `Firebird` | -| Ingres | `Ingres` | -| Sybase | `Sybase` | - -This is an inventory of dialects present in source, **not a guarantee that every server version, driver or operation is supported**. Some entries are legacy variants. Verify the combination you deploy against the [provider implementations](src/Migrator/Providers/Impl) and [provider tests](src/Migrator.Tests/Providers). - -## Comparison with other .NET frameworks - -Reviewed **22 September 2026**. Migrator's column describes this repository; the alternatives summarize their official documentation. These are workflow differences, not performance benchmarks or a ranking. - -| Capability | Migrator.NET (this fork) | FluentMigrator | EF Core | DbUp | Evolve | -| ---------------------------- | --------------------------------- | -------------------------------------------- | ------------------------------------ | -------------------------- | --------------------------------- | -| Authoring | Handwritten C# transformation API | Handwritten C# fluent DSL | C# scaffolded from model differences | SQL or C# scripts | Versioned SQL files | -| ORM-independent workflow | Yes | Yes | Uses EF model / DbContext | Yes | Yes | -| Model-difference scaffolding | No built-in generator | Hand-authored | Yes, with model snapshots | Hand-authored | Hand-authored | -| Downgrade applied migrations | Authored `Down()` | `Down()`; supported auto-reverse expressions | Generated/editable `Down()` | Custom undo or forward fix | Forward fix; no Down command | -| Separate histories | Scope + selected assembly/types | Custom version table + filtering | Contexts + custom history table | Journals + script filters | Metadata table/schema + locations | -| Execution | Library / custom host | Library + CLI | CLI, scripts, bundles, runtime | Library / custom host | Library, .NET tool, CLI | -| Recurring work | Custom code | Maintenance migrations / profiles | Seeding APIs (EF 9+) | `RunAlways` scripts | Checksum-based repeatable SQL | - -All five can execute raw SQL. Transaction support depends on database capabilities: Migrator starts one per migration; DbUp makes transactions opt-in; the others have configurable transaction behavior. Reversing a completed migration is different from rolling back a failed transaction. Evolve's checksum-based repeatables also differ from always-run scripts or lifecycle hooks. - -- Choose **Migrator** for direct C# schema operations, scoped history and integration with your own host. -- Consider **FluentMigrator** for its fluent authoring API, packaged runners, tags and profiles. -- Consider **EF Core migrations** when your EF model drives the schema and you want scaffolding and deployment artifacts. -- Consider **DbUp** for a SQL-oriented runner composed in .NET, or **Evolve** for convention-based SQL with checksum validation and repeatables. - -Sources: [Migrator runner](src/Migrator/Migrator.cs), [FluentMigrator quick start](https://fluentmigrator.github.io/intro/quick-start.html) and [configuration](https://fluentmigrator.github.io/intro/configuration.html), [EF Core migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) and [deployment](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying), [DbUp documentation](https://dbup.readthedocs.io/en/latest/) and [script types](https://dbup.readthedocs.io/en/latest/more-info/script-types/), [Evolve concepts](https://evolve-db.netlify.app/concepts/). The [full homepage comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) includes transaction, provider and source details; its source is available in [docs/index.html](docs/index.html). - -## Building and testing - -```sh -dotnet restore Migrator.slnx -dotnet build Migrator.slnx --configuration Release --no-restore -``` - -Tests use NUnit. Run a focused runner test fixture without provisioning external databases: - -```sh -dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release --filter "FullyQualifiedName~Migrator.Tests.MigratorTest" -``` - -The full suite includes database integration tests: - -```sh -dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release -``` - -Use disposable test databases: integration tests create, alter and remove schema objects. Configure connections in `src/Migrator.Tests/appsettings.Development.json` using the structure and identifiers in [appsettings.json](src/Migrator.Tests/appsettings.json), and set `ASPNETCORE_ENVIRONMENT=Development`. The development settings file is gitignored; keep credentials there rather than committing them. - -The [.NET workflow](.github/workflows/dotnetpull.yml) documents CI database services and commands. Provider coverage varies; a passing build alone does not validate every supported database family. - -### Live database testing - -See [live database testing](docs/live-database-tests.md) for the CI matrix, pinned versions, local commands, coverage, engine limitations and excluded candidates. - -## Documentation and GitHub Pages - -The homepage in [`docs/`](docs/README.md) includes installation, a runnable quick start, provider information and a sourced feature comparison. It uses plain HTML, CSS and JavaScript with no build dependencies. - -Preview locally from the repository root: - -```sh -python -m http.server 8766 --directory docs --bind 127.0.0.1 -``` - -Open [localhost:8766](http://localhost:8766). To publish, select **GitHub Actions** under **Settings → Pages → Build and deployment**, then merge the site into `master`. The [Pages workflow](.github/workflows/pages.yml) deploys changes to `docs/` at [dotnetprojects.github.io/Migrator.NET](https://dotnetprojects.github.io/Migrator.NET/). The workflow can also be dispatched manually on `master`. - -## Contributing and project history - -Bug reports, provider fixes, tests and documentation improvements are welcome through [issues](https://github.com/dotnetprojects/Migrator.NET/issues) and [pull requests](https://github.com/dotnetprojects/Migrator.NET/pulls). Include the package version, database/driver versions, a minimal migration that reproduces the problem, and expected versus actual behavior. Add a focused regression test for a behavior change and run the relevant provider tests. - -This project continues the original [Migrator.NET](https://github.com/migratordotnet/Migrator.NET), which began on Google Code. This fork incorporates contributions from other forks and work on SQLite schema reading and recreation, composite primary keys, SQL Server index inspection, reserved identifiers, provider independence and migration scopes. - -## License - -The package declares **Mozilla Public License 1.1 (MPL-1.1)** in its [project metadata](src/Migrator/DotNetProjects.Migrator.csproj). See the [license text](https://www.mozilla.org/en-US/MPL/1.1/) and source-file notices. +# DotNetProjects.Migrator + +**Versioned database migrations in C#, independent of your ORM.** + +[![NuGet version](https://img.shields.io/nuget/v/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) +[![NuGet downloads](https://img.shields.io/nuget/dt/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) +[![Build and tests](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml) +[![GitHub Pages](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml) +[![Source target: .NET 9](https://img.shields.io/badge/source_target-.NET_9-512BD4)](src/Migrator/DotNetProjects.Migrator.csproj) +[![License: MPL-1.1](https://img.shields.io/badge/license-MPL--1.1-blue.svg)](https://www.mozilla.org/en-US/MPL/1.1/) + +[Homepage & documentation](https://dotnetprojects.github.io/Migrator.NET/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) + +DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratordotnet/Migrator.NET). Write each schema change as a numbered C# class, commit it alongside your application, and use the runner to bring a database to the required version. The database records which migrations have already been applied. + +## Contents + +- [Why use it?](#why-use-it) +- [Installation and requirements](#installation-and-requirements) +- [Quick start](#quick-start) +- [Migration versions and rollback](#migration-versions-and-rollback) +- [Multiple modules and migration scopes](#multiple-modules-and-migration-scopes) +- [Schema and data operations](#schema-and-data-operations) +- [Database providers](#database-providers) +- [Comparison with other .NET frameworks](#comparison-with-other-net-frameworks) +- [Building and testing](#building-and-testing) +- [Documentation and GitHub Pages](#documentation-and-github-pages) +- [Contributing and project history](#contributing-and-project-history) +- [License](#license) + +## Why use it? + +- **Explicit C# migrations.** Define forward and reverse changes with `Up()` and `Down()`; review them like application code. +- **No ORM dependency.** Use it alongside EF, Dapper, another data layer, or plain ADO.NET. +- **Database transformation API.** Work with tables, columns, keys, indexes and data, with raw SQL available for provider-specific operations. +- **Version tracking.** Apply pending migrations or target a specific version using database-backed history. +- **Scoped histories.** Track multiple modules in one database when each runner is given the appropriate migration set. +- **Bring your database driver.** The library does not directly reference database-driver packages; supply an ADO.NET connection or configure the driver factory. +- **SQLite schema handling.** This fork includes schema inspection and table-recreation logic for operations SQLite cannot perform directly. + +The source upgrade adds a structured fluent API, runner filtering/lifecycle options, SQL-preview subset, native locking, a CLI project and optional Microsoft DI/logging integration. These changes are under review and **are not a released NuGet feature claim**. See the [runner and fluent guide](docs/runner-guide.md) and [detailed framework comparison](docs/migration-framework-comparison.md). EF-style model scaffolding and migration-content checksums remain outside the implementation. + +## Installation and requirements + +```sh +dotnet add package DotNetProjects.Migrator +``` + +Install the ADO.NET driver for your database separately. For the SQLite example below: + +```sh +dotnet add package Microsoft.Data.Sqlite --version 9.0.7 +``` + +The **current source targets `net9.0`**. Check the [NuGet package's framework list](https://www.nuget.org/packages/DotNetProjects.Migrator/#supportedframeworks-body-tab) for the particular release you install; older package releases may target different frameworks. The SQLite driver version above matches the repository's test dependency. + +Building the `.slnx` solution requires an SDK that understands that format, such as .NET SDK 9.0.200 or later. The runtime required by the current source is .NET 9. + +## Quick start + +### 1. Create a migration host + +```sh +dotnet new console -n MigrationDemo -f net9.0 +cd MigrationDemo +dotnet add package DotNetProjects.Migrator +dotnet add package Microsoft.Data.Sqlite --version 9.0.7 +``` + +### 2. Add `CreateUsers.cs` + +Migrations must be public classes implementing the migration contract, decorated with `[Migration(version)]`. Each version must be unique within the set loaded by one runner. + +```csharp +using System.Data; +using DotNetProjects.Migrator.Framework; + +[Migration(1)] +public class CreateUsers : Migration +{ + public override void Up() + { + Database.AddTable("Users", + new Column("Id", DbType.Int32, ColumnProperty.NotNull), + new Column("Name", DbType.String, 255)); + Database.AddPrimaryKey("PK_Users", "Users", "Id"); + } + + public override void Down() + { + Database.RemoveTable("Users"); + } +} +``` + +### 3. Replace `Program.cs` + +```csharp +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Providers; +using Microsoft.Data.Sqlite; + +using var connection = new SqliteConnection("Data Source=app.db"); +connection.Open(); + +using var provider = ProviderFactory.Create( + ProviderTypes.SQLite, connection, defaultSchema: null); + +var migrator = new Migrator( + provider, typeof(CreateUsers).Assembly, trace: false); + +if (migrator.LastAppliedMigrationVersion is long applied + && applied > migrator.AssemblyLastMigrationVersion) +{ + throw new InvalidOperationException( + "Database version is newer than this application."); +} + +migrator.MigrateToLastVersion(); +``` + +### 4. Run it + +```sh +dotnet run +``` + +This creates a local SQLite database containing `Users` and the migration history table. Running the application again skips version `1` because it has already been recorded. Add a new class with `[Migration(2)]` for the next change. + +The example supplies an **open** `IDbConnection`. The caller owns that connection and disposes it after the provider. If you use the connection-string overload instead, the selected provider must be able to resolve the appropriate ADO.NET factory. + +## Migration versions and rollback + +Use increasing numeric versions, or the attribute's date-based constructor: + +```csharp +[Migration(2026, 9, 22, 12, 0, 0)] +``` + +Keep applied migration classes in source control. Change the schema with a new migration instead of editing an already applied one: history records the version, not a checksum of the migration's content. + +| API | Purpose | +| ------------------------------ | -------------------------------------------------------------------------------- | +| `MigrateToLastVersion()` | Apply through the latest version in the loaded migration set. | +| `MigrateTo(version)` | Move to a chosen version, invoking `Up()` or `Down()` as required. | +| `AppliedMigrations` | List the versions recorded for the provider's scope. | +| `LastAppliedMigrationVersion` | Highest applied version, or `null` when none are applied. | +| `AssemblyLastMigrationVersion` | Highest version in the loaded migration set. | +| `SchemaInfoTableName` | Customize the history table name before accessing history or running migrations. | + +With the runner above, `migrator.MigrateTo(0)` reverses all applied migrations in its set. In this example that drops `Users`, including its data. A `Down()` implementation is a reverse schema operation, not a backup restore. + +Migration execution starts a transaction for each migration and attempts rollback on failure. Actual atomicity depends on the database, driver and operation; some databases implicitly commit DDL. `AfterUp()` and `AfterDown()` run **after commit**, so a failure in those hooks cannot undo the committed migration. + +For deployment, run a dedicated migration host before the application needs the new schema. Coordinate it so competing instances do not migrate the same database concurrently. Review and test both directions against your actual database engine. + +## Multiple modules and migration scopes + +The default history table is `SchemaInfo`, with version, scope and timestamp information. The default scope is `"default"`. You can use separate scopes for modules sharing a database. + +Within a host with an open `connection`, select the module's migration types explicitly: + +```csharp +using var billingProvider = ProviderFactory.Create( + ProviderTypes.SQLite, + connection, + defaultSchema: null, + scope: "billing"); + +var billingMigrator = new Migrator( + billingProvider, + false, + typeof(Billing001), + typeof(Billing002)); + +billingMigrator.MigrateToLastVersion(); +``` + +`Billing001` and `Billing002` represent your own public migration classes. Alternatively, give the runner an assembly that contains only that module's migrations. + +Important details: + +- 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). + +## Fluent API and deployment tooling + +Run the [compiled fluent example](examples/FluentQuickStart/Program.cs): + +```sh +dotnet run --project examples/FluentQuickStart +``` + +The example creates a complete table definition, previews it without changing history, runs a whole-session migration, then verifies automatic reversal. The [runner guide](docs/runner-guide.md) covers CLI commands, tags/profiles, maintenance, transactions, optional DI/logging, locks and preview limitations. Build the source packages locally to try the new tooling; no NuGet publication accompanies these PRs. + +## Schema and data operations + +Inside a migration, `Database` implements [`ITransformationProvider`](src/Migrator/Framework/ITransformationProvider.cs). It includes: + +| Area | Examples | +| ------------------ | ------------------------------------------------------------------------------------- | +| Tables and columns | `AddTable`, `RemoveTable`, `RenameTable`, `AddColumn`, `ChangeColumn`, `RemoveColumn` | +| Keys and indexes | `AddPrimaryKey`, `AddForeignKey`, `AddIndex` and corresponding removal operations | +| Schema inspection | `TableExists`, `ColumnExists`, `GetTables`, `GetColumns` | +| Data and SQL | `Insert`, `Update`, `Delete`, `ExecuteNonQuery`, `ExecuteQuery`, `ExecuteScalar` | + +For example, a new migration can add a column: + +```csharp +public override void Up() +{ + Database.AddColumn("Users", new Column("Email", DbType.String, 320)); +} + +public override void Down() +{ + Database.RemoveColumn("Users", "Email"); +} +``` + +Provider implementations determine which operations are available and how they map to SQL. Use `Database.ExecuteNonQuery(...)` for custom SQL and keep dialect-specific statements explicit. The source also includes a [schema builder API](src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs). + +## Database providers + +The [provider factory](src/Migrator/ProviderFactory.cs) contains these database families: + +| Database | `ProviderTypes` value(s) | +| ------------ | ---------------------------- | +| SQL Server | `SqlServer`, `SqlServer2005` | +| PostgreSQL | `PostgreSQL`, `PostgreSQL82` | +| SQLite | `SQLite`, `MonoSQLite` | +| MySQL | `Mysql` | +| MariaDB | `MariaDB` | +| Oracle | `Oracle`, `MsOracle` | +| IBM Db2 | `IBM_DB2` | +| IBM Informix | `IBM_Informix` | +| Firebird | `Firebird` | +| Ingres | `Ingres` | +| Sybase | `Sybase` | + +This is an inventory of dialects present in source, **not a guarantee that every server version, driver or operation is supported**. Some entries are legacy variants. Verify the combination you deploy against the [provider implementations](src/Migrator/Providers/Impl) and [provider tests](src/Migrator.Tests/Providers). + +## Comparison with other .NET frameworks + +Reviewed **22 September 2026**. Migrator's column describes this repository; the alternatives summarize their official documentation. These are workflow differences, not performance benchmarks or a ranking. + +| Capability | Migrator.NET (this fork) | FluentMigrator | EF Core | DbUp | Evolve | +| ---------------------------- | --------------------------------- | -------------------------------------------- | ------------------------------------ | -------------------------- | --------------------------------- | +| Authoring | Handwritten C# transformation API | Handwritten C# fluent DSL | C# scaffolded from model differences | SQL or C# scripts | Versioned SQL files | +| ORM-independent workflow | Yes | Yes | Uses EF model / DbContext | Yes | Yes | +| Model-difference scaffolding | No built-in generator | Hand-authored | Yes, with model snapshots | Hand-authored | Hand-authored | +| Downgrade applied migrations | Authored `Down()` | `Down()`; supported auto-reverse expressions | Generated/editable `Down()` | Custom undo or forward fix | Forward fix; no Down command | +| Separate histories | Scope + selected assembly/types | Custom version table + filtering | Contexts + custom history table | Journals + script filters | Metadata table/schema + locations | +| Execution | Library / custom host | Library + CLI | CLI, scripts, bundles, runtime | Library / custom host | Library, .NET tool, CLI | +| Recurring work | Custom code | Maintenance migrations / profiles | Seeding APIs (EF 9+) | `RunAlways` scripts | Checksum-based repeatable SQL | + +All five can execute raw SQL. Transaction support depends on database capabilities: Migrator starts one per migration; DbUp makes transactions opt-in; the others have configurable transaction behavior. Reversing a completed migration is different from rolling back a failed transaction. Evolve's checksum-based repeatables also differ from always-run scripts or lifecycle hooks. + +- Choose **Migrator** for direct C# schema operations, scoped history and integration with your own host. +- Consider **FluentMigrator** for its fluent authoring API, packaged runners, tags and profiles. +- Consider **EF Core migrations** when your EF model drives the schema and you want scaffolding and deployment artifacts. +- Consider **DbUp** for a SQL-oriented runner composed in .NET, or **Evolve** for convention-based SQL with checksum validation and repeatables. + +Sources: [Migrator runner](src/Migrator/Migrator.cs), [FluentMigrator quick start](https://fluentmigrator.github.io/intro/quick-start.html) and [configuration](https://fluentmigrator.github.io/intro/configuration.html), [EF Core migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) and [deployment](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying), [DbUp documentation](https://dbup.readthedocs.io/en/latest/) and [script types](https://dbup.readthedocs.io/en/latest/more-info/script-types/), [Evolve concepts](https://evolve-db.netlify.app/concepts/). The [full homepage comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) includes transaction, provider and source details; its source is available in [docs/index.html](docs/index.html). + +## Building and testing + +```sh +dotnet restore Migrator.slnx +dotnet build Migrator.slnx --configuration Release --no-restore +``` + +Tests use NUnit. Run a focused runner test fixture without provisioning external databases: + +```sh +dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release --filter "FullyQualifiedName~Migrator.Tests.MigratorTest" +``` + +The full suite includes database integration tests: + +```sh +dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release +``` + +Use disposable test databases: integration tests create, alter and remove schema objects. Configure connections in `src/Migrator.Tests/appsettings.Development.json` using the structure and identifiers in [appsettings.json](src/Migrator.Tests/appsettings.json), and set `ASPNETCORE_ENVIRONMENT=Development`. The development settings file is gitignored; keep credentials there rather than committing them. + +The [.NET workflow](.github/workflows/dotnetpull.yml) documents CI database services and commands. Provider coverage varies; a passing build alone does not validate every supported database family. + +### Live database testing + +See [live database testing](docs/live-database-tests.md) for the CI matrix, pinned versions, local commands, coverage, engine limitations and excluded candidates. + +## Documentation and GitHub Pages + +The homepage in [`docs/`](docs/README.md) includes installation, a runnable quick start, provider information and a sourced feature comparison. It uses plain HTML, CSS and JavaScript with no build dependencies. + +Preview locally from the repository root: + +```sh +python -m http.server 8766 --directory docs --bind 127.0.0.1 +``` + +Open [localhost:8766](http://localhost:8766). To publish, select **GitHub Actions** under **Settings → Pages → Build and deployment**, then merge the site into `master`. The [Pages workflow](.github/workflows/pages.yml) deploys changes to `docs/` at [dotnetprojects.github.io/Migrator.NET](https://dotnetprojects.github.io/Migrator.NET/). The workflow can also be dispatched manually on `master`. + +## Contributing and project history + +Bug reports, provider fixes, tests and documentation improvements are welcome through [issues](https://github.com/dotnetprojects/Migrator.NET/issues) and [pull requests](https://github.com/dotnetprojects/Migrator.NET/pulls). Include the package version, database/driver versions, a minimal migration that reproduces the problem, and expected versus actual behavior. Add a focused regression test for a behavior change and run the relevant provider tests. + +This project continues the original [Migrator.NET](https://github.com/migratordotnet/Migrator.NET), which began on Google Code. This fork incorporates contributions from other forks and work on SQLite schema reading and recreation, composite primary keys, SQL Server index inspection, reserved identifiers, provider independence and migration scopes. + +## License + +The package declares **Mozilla Public License 1.1 (MPL-1.1)** in its [project metadata](src/Migrator/DotNetProjects.Migrator.csproj). See the [license text](https://www.mozilla.org/en-US/MPL/1.1/) and source-file notices. diff --git a/docs/index.html b/docs/index.html index 768b513a..99cccd77 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,699 +1,704 @@ - - - - - - - - Migrator.NET — Database changes, in your code. - - - - - - - -
-
-
-
-

DOTNETPROJECTS / MIGRATOR.NET

-

Database changes.
Part of your code.

-

- Write schema changes in C#. Version them with your application. - Run them with the database provider and ORM you choose. -

- -

- Open source · MPL-1.1 · Current source targets .NET 9 -

-
-
-
- - 001_CreateUsers.csUP / DOWN -
-
[Migration(1)]
-public class CreateUsers : Migration
-{
-    public override void Up()
-    {
-        Database.AddTable("Users",
-            new Column("Id", DbType.Int32,
-                ColumnProperty.NotNull),
-            new Column("Name", DbType.String, 255));
-        Database.AddPrimaryKey("PK_Users", "Users", "Id");
-    }
-
-    public override void Down()
-    {
-        Database.RemoveTable("Users");
-    }
-}
- -
-
-
-
-
- PROVIDER DIALECTSSQL ServerPostgreSQLSQLiteMySQL / MariaDBOracleSee all → -
-
-
-

SMALL API. EXPLICIT CONTROL.

-

- Your schema has a history.
Keep it in the repository. -

-
-
- 01 / AUTHOR -

C# without an ORM dependency

-

- Define tables, columns, indexes and constraints through a - transformation API. Use raw SQL when a change needs - database-specific behavior. -

-
-
- 02 / VERSION -

Move forward. Step back.

-

- Number your migrations, implement Up() and - Down(), and migrate to a chosen version. Applied - migrations are recorded in the database. -

-
-
- 03 / ORGANIZE -

Separate histories by scope

-

- Keep module version histories in one database using named scopes. - Select each module’s migration assembly or types when you create - its runner. -

-
-
-
-
-
-
-
-

QUICK START

-

From code to schema.

-
-

- A minimal SQLite example.
Use a .NET 9 console project for - the current source. -

-
-
-
- 1 -

Install the packages

-

- Add Migrator and an ADO.NET driver. This example passes an open - connection directly to the provider. -

- View package versions on NuGet ↗ -
-
-
- Terminal -
-
dotnet new console -n MigrationDemo -f net9.0
-cd MigrationDemo
-dotnet add package DotNetProjects.Migrator
-dotnet add package Microsoft.Data.Sqlite --version 9.0.7
-
-
-
-
- 2 -

Describe the change

-

- Add a public migration class. Each version must be unique within - the migration set loaded by a runner. -

-

- Down() is your explicit reverse operation; dropping - a table also removes its data. -

-
-
-
- CreateUsers.cs -
-
using System.Data;
-using DotNetProjects.Migrator.Framework;
-
-[Migration(1)]
-public class CreateUsers : Migration
-{
-    public override void Up()
-    {
-        Database.AddTable("Users",
-            new Column("Id", DbType.Int32,
-                ColumnProperty.NotNull),
-            new Column("Name", DbType.String, 255));
-        Database.AddPrimaryKey("PK_Users", "Users", "Id");
-    }
-
-    public override void Down()
-    {
-        Database.RemoveTable("Users");
-    }
-}
-
-
-
-
- 3 -

Run pending migrations

-

- Replace Program.cs with this code, then run - dotnet run. The runner discovers the migration in - your assembly and records it under the default scope. -

-

- Subsequent runs skip applied versions. Use - MigrateTo(version) to target an earlier or later - version. -

-
-
-
- Program.cs -
-
using DotNetProjects.Migrator;
-using DotNetProjects.Migrator.Providers;
-using Microsoft.Data.Sqlite;
-
-using var connection = new SqliteConnection("Data Source=app.db");
-connection.Open();
-
-using var provider = ProviderFactory.Create(
-    ProviderTypes.SQLite, connection, defaultSchema: null);
-
-var migrator = new Migrator(
-    provider, typeof(CreateUsers).Assembly, trace: false);
-
-if (migrator.LastAppliedMigrationVersion is long applied
-    && applied > migrator.AssemblyLastMigrationVersion)
-{
-    throw new InvalidOperationException(
-        "Database version is newer than this application.");
-}
-
-migrator.MigrateToLastVersion();
-
-
- -
-
-
-
-
-

DATABASE PROVIDERS

-

One API. Multiple dialects.

-
-

- Supply your ADO.NET driver.
Migrator supplies the schema - operations. -

-
-
-
-

Common database families

-
    -
  • SQL Server
  • -
  • PostgreSQL
  • -
  • SQLite
  • -
  • MySQL
  • -
  • MariaDB
  • -
  • Oracle
  • -
-
-
-

Additional dialects in source

-
    -
  • IBM Db2
  • -
  • IBM Informix
  • -
  • Firebird
  • -
  • Ingres
  • -
  • Sybase
  • -
-
-
-

- This is an implementation inventory, not a certification of every - server or driver version. Schema operations and transactional DDL vary - by provider. Check the - provider factory - and - provider tests - for your database. -

-
-
-
-
-
-

THE .NET MIGRATION LANDSCAPE

-

Choose by how you work.

-
-

- Feature comparison · Reviewed 22 September 2026
Read the sources and qualifications ↓ -

-
-

- Migrator fits applications that want explicit C# migrations and - scoped history without coupling schema changes to an ORM. Other - tools offer different authoring and deployment workflows. -

-

- Read the detailed feature comparison (Markdown) →
- Explore SQLite emulation, preservation limits and framework - differences → -

-

- Scroll horizontally to compare all five frameworks on smaller - screens. -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Built-in capabilities and documented workflows. “Custom” means - application code or configuration is needed. -
Capability - Migrator.NET DotNetProjects forkSource [1] - - FluentMigrator Sources [2] - - EF Core Sources [3] - - DbUp Sources [4] - - Evolve Sources [5] -
Authoring styleHandwritten C#
Transformation API
Handwritten C#
Fluent DSL
C# generated from model changes; editableSQL scripts; C# scripts also supportedVersioned SQL files
ORM-independent workflowYesYesUses EF model and DbContextYesYes
- Generate migrations from model differences - No built-in generatorHand-authoredYes — model snapshotsHand-authoredHand-authored
Raw SQLExecuteNonQueryExecute.Sql / scriptsmigrationBuilder.SqlPrimary workflowPrimary workflow
Downgrade an applied version - Authored Down()
MigrateTo -
- Down(); auto-reverse for supported expressions - Down(); target an earlier migrationForward fixes; custom undo workflowForward fixes; no Down command
History / module separation - Scope in history table + selected assembly/types - Custom version tables + migration filtering - Separate contexts / migrations + custom history tables - Separate journals + script filteringMetadata table/schema + script locations
TransactionsPer migrationPer migration by default; configurableMost migrations wrapped automaticallyOpt-in per script or whole run; none by defaultPer migration by default; whole-run option
Execution / deploymentLibrary; write your own hostIn-process runner + CLICLI, SQL scripts, bundles, runtime APILibrary; host in a console app or application.NET library, .NET tool, CLI
Database abstractionProvider dialects for schema operationsProvider-specific SQL generators - Relational providers; migrations may differ by provider - Database integrations; you write dialect-specific SQLDatabase integrations; you write dialect-specific SQL
Repeatable / recurring workCustom application codeMaintenance migrations / profilesSeeding APIs (EF 9+); custom codeRunAlways scriptsRepeatable SQL reruns on checksum change
-
-
-

- Rollback has two meanings. Reversing an already - applied migration uses authored reverse operations. Rolling back a - failed transaction depends on the database’s DDL support. Neither - restores data removed by a successful destructive migration. -

-

- Recurring work is not the same as change detection. - Evolve stores script checksums and validates changes; Migrator - records versions and scopes without built-in content checksum - validation. Maintenance hooks, seeding and RunAlways have - different execution rules. -

-
-
-
-

Keep migrations in C#

-

- Migrator: direct schema operations, scoped - history, and integration through your own host. - FluentMigrator: a fluent DSL with packaged - runners, tags and profiles. -

-
-
-

Let the model drive changes

-

- EF Core: a natural fit when an EF model defines - your schema and you want migration scaffolding, SQL generation - and deployment bundles. -

-
-
-

Keep SQL as the source

-

- DbUp: compose a script runner in .NET. - Evolve: convention-based versioned SQL, - checksum validation and repeatable scripts. -

-
-
-
- Sources & comparison methodology -

- Our column is based on the current repository source, which - targets net9.0. Other columns summarize official - documentation reviewed on 22 September 2026, rather than claiming - parity across every released package. Check your chosen release, - provider and database version. Suitability notes are our - interpretation of these documented capabilities. -

-
    -
  1. - DotNetProjects.Migrator: - target framework, - runner, - execution and transactions, - history and schema operations, - migration discovery. -
  2. -
  3. - FluentMigrator: - quick start and runners, - configuration and version tables, - auto-reversing migrations, - maintenance migrations, - profiles, - authoring and providers. -
  4. -
  5. - EF Core: - model snapshots, - authoring and transactions, - scripts, bundles and downgrade, - custom history tables, - multiple providers, - seeding. -
  6. -
  7. - DbUp: - execution, - transactions, - journaling, - script types, - forward-change philosophy, - SQL and C# script providers. -
  8. -
  9. - Evolve: - commands, checksums, repeatables and transactions, - configuration, - execution options. -
  10. -
-
-
-
-
-
-

CONTINUING MIGRATOR.NET

-

A familiar idea.
A maintained fork.

-

- DotNetProjects.Migrator continues the original Migrator.NET project, - bringing together fork contributions with work on SQLite schema - handling, provider independence and migration scopes. -

-
- -
-
- - - - + + + + + + + + Migrator.NET — Database changes, in your code. + + + + + + + +
+
+
+
+

DOTNETPROJECTS / MIGRATOR.NET

+

Database changes.
Part of your code.

+

+ Write schema changes in C#. Version them with your application. + Run them with the database provider and ORM you choose. +

+ +

+ Open source · MPL-1.1 · Current source targets .NET 9 +

+
+
+
+ + 001_CreateUsers.csUP / DOWN +
+
[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32,
+                ColumnProperty.NotNull),
+            new Column("Name", DbType.String, 255));
+        Database.AddPrimaryKey("PK_Users", "Users", "Id");
+    }
+
+    public override void Down()
+    {
+        Database.RemoveTable("Users");
+    }
+}
+ +
+
+
+
+
+ PROVIDER DIALECTSSQL ServerPostgreSQLSQLiteMySQL / MariaDBOracleSee all → +
+
+
+

SMALL API. EXPLICIT CONTROL.

+

+ Your schema has a history.
Keep it in the repository. +

+
+
+ 01 / AUTHOR +

C# without an ORM dependency

+

+ Define tables, columns, indexes and constraints through a + transformation API. Use raw SQL when a change needs + database-specific behavior. +

+
+
+ 02 / VERSION +

Move forward. Step back.

+

+ Number your migrations, implement Up() and + Down(), and migrate to a chosen version. Applied + migrations are recorded in the database. +

+
+
+ 03 / ORGANIZE +

Separate histories by scope

+

+ Keep module version histories in one database using named scopes. + Select each module’s migration assembly or types when you create + its runner. +

+
+
+
+
+
+
+
+

QUICK START

+

From code to schema.

+
+

+ A minimal SQLite example.
Use a .NET 9 console project for + the current source. +

+
+
+
+ 1 +

Install the packages

+

+ Add Migrator and an ADO.NET driver. This example passes an open + connection directly to the provider. +

+ View package versions on NuGet ↗ +
+
+
+ Terminal +
+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7
+
+
+
+
+ 2 +

Describe the change

+

+ Add a public migration class. Each version must be unique within + the migration set loaded by a runner. +

+

+ Down() is your explicit reverse operation; dropping + a table also removes its data. +

+
+
+
+ CreateUsers.cs +
+
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32,
+                ColumnProperty.NotNull),
+            new Column("Name", DbType.String, 255));
+        Database.AddPrimaryKey("PK_Users", "Users", "Id");
+    }
+
+    public override void Down()
+    {
+        Database.RemoveTable("Users");
+    }
+}
+
+
+
+
+ 3 +

Run pending migrations

+

+ Replace Program.cs with this code, then run + dotnet run. The runner discovers the migration in + your assembly and records it under the default scope. +

+

+ Subsequent runs skip applied versions. Use + MigrateTo(version) to target an earlier or later + version. +

+
+
+
+ Program.cs +
+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+
+using var provider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var migrator = new Migrator(
+    provider, typeof(CreateUsers).Assembly, trace: false);
+
+if (migrator.LastAppliedMigrationVersion is long applied
+    && applied > migrator.AssemblyLastMigrationVersion)
+{
+    throw new InvalidOperationException(
+        "Database version is newer than this application.");
+}
+
+migrator.MigrateToLastVersion();
+
+
+ +
+
+
+
+
+

DATABASE PROVIDERS

+

One API. Multiple dialects.

+
+

+ Supply your ADO.NET driver.
Migrator supplies the schema + operations. +

+
+
+
+

Common database families

+
    +
  • SQL Server
  • +
  • PostgreSQL
  • +
  • SQLite
  • +
  • MySQL
  • +
  • MariaDB
  • +
  • Oracle
  • +
+
+
+

Additional dialects in source

+
    +
  • IBM Db2
  • +
  • IBM Informix
  • +
  • Firebird
  • +
  • Ingres
  • +
  • Sybase
  • +
+
+
+

+ This is an implementation inventory, not a certification of every + server or driver version. Schema operations and transactional DDL vary + by provider. Check the + provider factory + and + provider tests + for your database. +

+
+
+
+
+
+

THE .NET MIGRATION LANDSCAPE

+

Choose by how you work.

+
+

+ Feature comparison · Reviewed 22 September 2026
Read the sources and qualifications ↓ +

+
+

Source upgrade under review, not a NuGet release: + fluent operations, SQL-preview subset, runner options, native locks and source CLI. + Read the runner guide and limitations. + Follow the PR stack. +

+

+ Migrator fits applications that want explicit C# migrations and + scoped history without coupling schema changes to an ORM. Other + tools offer different authoring and deployment workflows. +

+

+ Read the detailed feature comparison (Markdown) →
+ Explore SQLite emulation, preservation limits and framework + differences → +

+

+ Scroll horizontally to compare all five frameworks on smaller + screens. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Built-in capabilities and documented workflows. “Custom” means + application code or configuration is needed. +
Capability + Migrator.NET DotNetProjects forkSource [1] + + FluentMigrator Sources [2] + + EF Core Sources [3] + + DbUp Sources [4] + + Evolve Sources [5] +
Authoring styleImperative C# + structured fluent APIHandwritten C#
Fluent DSL
C# generated from model changes; editableSQL scripts; C# scripts also supportedVersioned SQL files
ORM-independent workflowYesYesUses EF model and DbContextYesYes
+ Generate migrations from model differences + No built-in generatorHand-authoredYes — model snapshotsHand-authoredHand-authored
Raw SQLExecuteNonQueryExecute.Sql / scriptsmigrationBuilder.SqlPrimary workflowPrimary workflow
Downgrade an applied version + Authored Down() or supported automatic reversal + + Down(); auto-reverse for supported expressions + Down(); target an earlier migrationForward fixes; custom undo workflowForward fixes; no Down command
History / module separation + Scope-filtered discovery + history + Custom version tables + migration filtering + Separate contexts / migrations + custom history tables + Separate journals + script filteringMetadata table/schema + script locations
TransactionsPer migration; none or verified whole-session modesPer migration by default; configurableMost migrations wrapped automaticallyOpt-in per script or whole run; none by defaultPer migration by default; whole-run option
Execution / deploymentLibrary + source CLI (unreleased)In-process runner + CLICLI, SQL scripts, bundles, runtime APILibrary; host in a console app or application.NET library, .NET tool, CLI
Database abstractionProvider dialects for schema operationsProvider-specific SQL generators + Relational providers; migrations may differ by provider + Database integrations; you write dialect-specific SQLDatabase integrations; you write dialect-specific SQL
Repeatable / recurring workOrdered maintenance + named profiles; no checksum repeatablesMaintenance migrations / profilesSeeding APIs (EF 9+); custom codeRunAlways scriptsRepeatable SQL reruns on checksum change
+
+
+

+ Rollback has two meanings. Reversing an already + applied migration uses authored reverse operations. Rolling back a + failed transaction depends on the database’s DDL support. Neither + restores data removed by a successful destructive migration. +

+

+ Recurring work is not the same as change detection. + Evolve stores script checksums and validates changes; Migrator + records versions and scopes without built-in content checksum + validation. Maintenance hooks, seeding and RunAlways have + different execution rules. +

+
+
+
+

Keep migrations in C#

+

+ Migrator: direct schema operations, scoped + history, and integration through your own host. + FluentMigrator: a fluent DSL with packaged + runners, tags and profiles. +

+
+
+

Let the model drive changes

+

+ EF Core: a natural fit when an EF model defines + your schema and you want migration scaffolding, SQL generation + and deployment bundles. +

+
+
+

Keep SQL as the source

+

+ DbUp: compose a script runner in .NET. + Evolve: convention-based versioned SQL, + checksum validation and repeatable scripts. +

+
+
+
+ Sources & comparison methodology +

+ Our column is based on the current repository source, which + targets net9.0. Other columns summarize official + documentation reviewed on 22 September 2026, rather than claiming + parity across every released package. Check your chosen release, + provider and database version. Suitability notes are our + interpretation of these documented capabilities. +

+
    +
  1. + DotNetProjects.Migrator: + target framework, + runner, + execution and transactions, + history and schema operations, + migration discovery. +
  2. +
  3. + FluentMigrator: + quick start and runners, + configuration and version tables, + auto-reversing migrations, + maintenance migrations, + profiles, + authoring and providers. +
  4. +
  5. + EF Core: + model snapshots, + authoring and transactions, + scripts, bundles and downgrade, + custom history tables, + multiple providers, + seeding. +
  6. +
  7. + DbUp: + execution, + transactions, + journaling, + script types, + forward-change philosophy, + SQL and C# script providers. +
  8. +
  9. + Evolve: + commands, checksums, repeatables and transactions, + configuration, + execution options. +
  10. +
+
+
+
+
+
+

CONTINUING MIGRATOR.NET

+

A familiar idea.
A maintained fork.

+

+ DotNetProjects.Migrator continues the original Migrator.NET project, + bringing together fork contributions with work on SQLite schema + handling, provider independence and migration scopes. +

+
+ +
+
+ + + + diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index 1894c4ae..e9748021 100644 --- a/docs/migration-framework-comparison.md +++ b/docs/migration-framework-comparison.md @@ -1,416 +1,410 @@ -# .NET database migration frameworks: detailed feature comparison - -**Reviewed: 22 September 2026.** This is a capability comparison, not a benchmark or an overall ranking. - -The main matrices cover **DotNetProjects.Migrator, FluentMigrator, EF Core migrations, DbUp and Evolve**—all five frameworks on the homepage. Additional sections cover **EF6, grate and RoundhousE**, with a short boundary comparison for **Flyway and Liquibase**. This is a defined shortlist, not a claim to catalogue every migration package ever published. - -Migrator findings are pinned to repository commit [`ab3aa9f`][m-revision], before the parallel refactoring. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. - -[Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Project README](../README.md) · [SQLite emulation comparison](#sqlite-emulation-comparison) · [Source index](#source-index) - -## Contents - -- [How to read the matrices](#how-to-read-the-matrices) -- [Authoring and application integration](#authoring-and-application-integration) -- [Schema and data operations](#schema-and-data-operations) -- [History, ordering and repeatability](#history-ordering-and-repeatability) -- [Transactions, rollback and coordination](#transactions-rollback-and-coordination) -- [Deployment, inspection and configuration](#deployment-inspection-and-configuration) -- [Database coverage and portability](#database-coverage-and-portability) -- [SQLite emulation comparison](#sqlite-emulation-comparison) -- [EF6, grate and RoundhousE](#ef6-grate-and-roundhouse) -- [Flyway and Liquibase in a .NET deployment](#flyway-and-liquibase-in-a-net-deployment) -- [Choosing a framework and identifying Migrator gaps](#choosing-a-framework-and-identifying-migrator-gaps) -- [Validation and maintenance](#validation-and-maintenance) -- [Source index](#source-index) - -## How to read the matrices - -| Term | Meaning | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Built-in / named API | The reviewed tool provides this operation or workflow. Database restrictions still apply. | -| Configure | Available through documented runner settings, composition or extension points. | -| Custom | You supply application code, SQL or deployment orchestration. Not automatic framework behavior. | -| No built-in | No implementation in the inspected Migrator source, or no equivalent in the reviewed documented workflow. It does not rule out third-party extensions. | -| Provider-dependent | Availability or semantics depend on the database integration and release. | -| Not verified | Evidence is insufficient for a positive or negative compatibility claim. | - -A SQL runner can execute a hand-authored table rebuild; that does **not** mean it automatically emulates `AlterColumn`. Likewise, recording applied migrations is not schema-drift detection, a transaction is not a deployment mutex, and a version downgrade is not a data restore. - -## Authoring and application integration - -Evidence: [Migrator runner][m-runner], [loader][m-loader], [migration contract][m-migration]; [FluentMigrator quick start][f-start] and [SQL execution][f-sql]; [EF Core overview][ef-overview] and [managing migrations][ef-managing]; [DbUp usage][d-usage] and [script providers][d-providers]; [Evolve concepts][e-concepts] and [configuration][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| ---------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------- | ------------------------------------- | ------------------------------------------ | -| Primary authoring artifact | Public C# migration class | C# migration class with fluent expressions | Generated, editable C# migration + model snapshot | SQL file or C# `IScript` | Versioned SQL file | -| Requires an ORM model | No | No | Yes, for normal scaffolding | No | No | -| Generates changes from model differences | No built-in | No built-in model differ in core workflow | Yes | No; author scripts | No; author scripts | -| Migration without a model change | Yes | Yes | Empty migration, then custom operations | Yes | Yes | -| Schema DSL / transformation API | `Database` operations; optional `SchemaBuilder` | Fluent create/alter/delete expressions | `MigrationBuilder` operations | No schema DSL; SQL / commands | No schema DSL; SQL | -| Custom C# logic | `Up` / `Down`; open provider | Migration code / connection operations | SQL/custom operations for database work | `IScript` and command factory | Surrounding host logic; migrations are SQL | -| Raw SQL | Command, query and scalar APIs | Inline, file and embedded SQL | `migrationBuilder.Sql` | Primary workflow | Primary workflow | -| Migration discovery | Assembly scan or explicit `Type[]` | Assembly scanning / filters | Context's migration assembly | Configurable script providers | Locations or embedded resources | -| Constructor dependency injection | Default loader uses `Activator.CreateInstance`; customize loader | Runner/DI integration | Context services; migration customization is separate | Custom script provider/host if needed | No C# migration constructors | -| Embedded execution | Yes | Yes | Yes | Yes | Library mode | -| Dedicated execution host | Write your own | Library or packaged runner | Tooling, bundles or custom host | Write your own | CLI, .NET tool or library | - -EF Core's model snapshot comparison is not a live-database schema comparison. DbUp's C# support is more than static SQL file loading, but it does not supply a cross-database schema-operation layer. - -## Schema and data operations - -This table separates having an authoring API from that API working identically on every engine. SQLite is broken out below. Evidence: [Migrator interface][m-api] and [provider factory][m-factory]; [FluentMigrator operations][f-start]; [EF Core migration operations][ef-managing]; [DbUp script execution][d-usage]; [Evolve SQL model][e-concepts]. - -| Operation family | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| --------------------------------------- | ---------------------------------------- | ----------------------------------- | ------------------------------------------ | ----------------------------- | ------------------------------------- | -| Create / drop table | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | -| Rename table | Schema API | Fluent API | Migration operation | Author SQL | Author SQL | -| Add / drop / rename column | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | -| Change type / nullability / default | `ChangeColumn` and default API | Alter expressions | `AlterColumn` | Author SQL | Author SQL | -| Primary / composite keys | API; provider-dependent | Fluent expressions | Migration operations | Author SQL | Author SQL | -| Foreign keys / delete behavior | API; mapped constraint types | Fluent expressions | Migration operations | Author SQL | Author SQL | -| Unique constraints | API | Fluent expressions | Migration operations | Author SQL | Author SQL | -| Check constraints | API using SQL predicate | Provider/custom SQL as applicable | Migration operations | Author SQL | Author SQL | -| Indexes | API and index model | Fluent expressions | Operations / provider annotations | Author SQL | Author SQL | -| Filtered / included / clustered indexes | Provider-specific subsets | Provider-specific options | Provider-specific support | Engine-specific SQL | Engine-specific SQL | -| Views | `AddView` and SQL | Usually SQL | Usually SQL migrations | Author SQL | SQL; repeatables useful | -| Stored procedures / triggers | Raw SQL | SQL / connection operations | SQL / custom operations | Author SQL | Author SQL | -| Fixed-data insert / update / delete | Data API | Fluent data expressions | `InsertData` / `UpdateData` / `DeleteData` | SQL or C# | SQL | -| Transform existing data | SQL, provider reads/writes, copy helpers | SQL / connection operations | SQL / custom operations | SQL or C# | SQL | -| Live table / column existence | Existence and metadata APIs | Schema query API | SQL/custom code | SQL or C# | SQL | -| Full schema-drift report | No built-in | Not established by version tracking | Snapshot comparison alone is insufficient | Journal alone is insufficient | Checksums concern scripts, not schema | - -## History, ordering and repeatability - -Evidence: [Migrator loader][m-loader], [execution][m-execution] and [history storage][m-provider]; [FluentMigrator configuration][f-config], [maintenance][f-maintenance] and [profiles][f-profiles]; [EF Core overview][ef-overview], [history][ef-history] and [seeding][ef-seeding]; [DbUp journaling][d-journal], [script types][d-types] and [usage][d-usage]; [Evolve concepts][e-concepts] and [options][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| -------------------------- | ------------------------------------------------------- | -------------------------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------------- | -| Applied-change identity | Numeric version within scope | Migration version | Migration ID | Script name | Script metadata | -| Default history | `SchemaInfo` | Version table | `__EFMigrationsHistory` | E.g. `SchemaVersions` | `changelog` | -| History customization | Table name; scope column | Version-table metadata | Table/schema; custom services | Custom journal / table | Metadata table/schema | -| Independent modules | Scope + selected migrations | Separate history + filters | Contexts/assemblies + separate history | Filters + separate journals | Locations + separate metadata | -| Environment selection | Host selection; ignore attribute for assembly discovery | Tags / profiles / configuration | Context/deployment configuration | Filters / host | Locations / placeholders / host | -| Skip applied work | Version history | Version history | Migration history | Journal | Metadata | -| Applied-source checksum | No built-in | Not a core version-table guarantee | No script checksum journal | Standard journal tracks names; custom validation | Script checksums | -| Late lower-numbered change | Revisits missing versions up to target | Check runner policy | Do not assume IDs make diverging branches safe | Unrecorded scripts eligible; ordering matters | `OutOfOrder` | -| Repeat on content change | Custom | Not equivalent to maintenance/profiles | Not equivalent to seeding | Custom checksum-aware runner | Repeatable SQL | -| Always-run work | Host code; hooks are per executed migration | Maintenance / selected profiles | Seeding APIs, EF 9+ | `RunAlways` / `NullJournal` | Not identical to RunAlways | -| Existing-schema baseline | Custom verified history initialization | Custom baseline/runner strategy | Existing-schema workflow | `MarkAsExecuted` | `StartVersion` / skip options | -| Repair checksums | Not applicable | Not established by version history | Not applicable | Custom journal concern | `repair` | - -**Migrator scope detail:** `MigrationAttribute.Scope` changes where a history record is written; it does not filter assembly discovery. A runner reads its provider scope and checks duplicate versions across its entire loaded set. Use separate assemblies or explicit types, normally leaving the attribute scope unset. History isolation is not table isolation. [Loader][m-loader], [execution][m-execution], [provider][m-provider]. - -## Transactions, rollback and coordination - -Evidence: [Migrator execution][m-execution] and [runner][m-runner]; [FluentMigrator configuration][f-config] and [auto-reverse][f-reverse]; [EF Core management][ef-managing], [deployment][ef-applying] and [SQLite limitations][ef-sqlite]; [DbUp transactions][d-transactions] and [philosophy][d-philosophy]; [Evolve concepts][e-concepts] and [options][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| ------------------------------ | -------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------ | -| Default transaction unit | Per migration | Per migration; configurable | Version-sensitive: EF 9 grouped pending migrations, reverted in EF 10 | None | Per migration | -| Whole-run transaction | Not a runner option | Configure/orchestrate; check runner | Depends on version/operations | `WithTransaction()` | `CommitAll` | -| Per-change transaction opt-out | No migration attribute | Transaction behavior | Raw SQL suppression | Choose strategy / separate runs | Script opt-out | -| Failed DDL rollback | Engine-dependent | Engine-dependent | Engine-dependent | When enabled and supported | Engine-dependent | -| Reverse committed migration | Authored `Down()` | `Down()` | Generated/editable `Down()` | Custom undo / forward fix | Forward fix; no Down command | -| Generate reverse operations | No | Supported auto-reverse expressions | Scaffolding; review output | No schema reverse generator | No | -| Target earlier version | `MigrateTo` | Down/rollback APIs | Earlier target / reverse script | Custom | Target limits forward work, not undo | -| Restore deleted data | Backup / reconstruction | Same | Same | Same | Same | -| Cross-process coordination | No built-in migration lock found | Serialize deployment / application-lock pattern | Migration locking, EF 9+; execution-path dependent | Host/provider concern; journal is not a lock | Cluster setting; provider-dependent | -| Post-commit hooks | `AfterUp` / `AfterDown` | Maintenance stages | Host/seeding lifecycle; not direct equivalent | Host / ordered scripts | Host / ordered scripts | - -A scope, checksum, history primary key or ordinary database write lock does not prove that two deployments can safely run the entire sequence concurrently. Evolve's cluster setting must be checked for the selected provider; it is not a blanket SQLite session-lock guarantee. - -## Deployment, inspection and configuration - -Evidence: [Migrator runner][m-runner] and [execution][m-execution]; [FluentMigrator runners][f-start] and [configuration][f-config]; [EF Core deployment][ef-applying]; [DbUp usage][d-usage], [variables][d-variables] and [logging][d-logging]; [Evolve execution][e-start] and [options][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| ------------------------------------ | ------------------------------------------------- | ------------------------------------------ | --------------------------------- | -------------------------------- | ------------------------------------------------ | -| Packaged CLI | No | Yes | `dotnet ef` | Core library; custom host | Yes | -| Dedicated migration bundle generator | No; publish host | Package runner/migrations | Yes | Publish host | CLI distribution, not EF-style bundle generation | -| Review SQL without applying | No equivalent runner SQL generator | Preview/output | Scripts | Authored SQL / pending scripts | Authored SQL | -| Dry-run qualification | Skips bodies; still touches provider/transactions | Processor preview; user code needs care | Not a full side-effect simulation | Pending list / custom simulation | `RollbackAll` actually executes | -| Idempotent deployment SQL | Custom | Preview is not idempotent history guarding | Provider-dependent; not SQLite | Author SQL / use journal | Author SQL / use metadata | -| Status | Versions / loaded types | Runner/tool info | CLI / history APIs | Pending/executed APIs | `info` | -| Command timeout | Provider setting | Processor setting | Database/provider setting | Runner/provider setting | `CommandTimeout` | -| Logging | `ILogger` / writers | Logging integration | EF logging | `IUpgradeLog` / integrations | Host/CLI | -| SQL substitution | Custom | Script tokens | Custom logic | `$variable$` | `${placeholder}` | -| Deployment identity | Host connection | Runner connection | Migration connection | Host connection | Tool connection | - -**Migrator dry run is not an offline SQL preview.** Execution starts provider work while `Up()`/`Down()` are skipped. It cannot show SQL from those skipped bodies and should not be described as side-effect-free database validation. [Execution source][m-execution]. - -## Database coverage and portability - -| Framework | How support is supplied | What it does not guarantee | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -| Migrator | Source dialects + separate ADO.NET drivers. Live CI covers SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix and Sybase; Ingres is another source dialect. [CI guide][m-live]. | Every server/driver release, operation or arbitrary SQL construct. | -| FluentMigrator | Provider generators/processors. [Configuration][f-config]. | The same expression working on every engine. | -| EF Core | Relational provider packages. [Multiple providers][ef-providers]. | One provider's generated migrations working unchanged elsewhere. | -| DbUp | Database integrations. [Provider list][d-databases]. | SQL dialect translation. | -| Evolve | Database integrations. [Requirements][e-requirements]. | SQL translation or identical transactions. | - -A migration can compile yet require a table copy, lose an unsupported schema detail or fail on existing data. Compare the exact operation and data shape, not just database names. - -## SQLite emulation comparison - -### What emulation means - -SQLite has native table rename, column rename, add-column and (on sufficiently recent engines, subject to restrictions) drop-column operations. SQLite 3.53.0 added native `ALTER COLUMN … SET/DROP NOT NULL`; it still does not provide general type/default alteration or `ALTER TABLE ADD/DROP CONSTRAINT`. More complex changes require a replacement table, copying rows and rebuilding dependent objects. Native capabilities evolve independently of the .NET driver package. [SQLite ALTER TABLE reference][sqlite-alter]. - -Migrator reads the **live schema** into `SQLiteTableInfo`, modifies that representation and calls `RecreateTable`. It creates `Temp`, copies mapped columns with `INSERT … SELECT`, drops the original, renames the replacement and recreates represented indexes. This works without an ORM model, but depends on what its schema reader can represent. [Implementation][m-sqlite], [schema model][m-sqlite-model]. - -### Automatic operation matrix - -**R** = built-in rebuild; **N** = native SQL path, subject to engine restrictions; **U** = unique-index substitution; **Manual** = author the change/rebuild yourself; **Manual** also covers a generated statement that the engine does not support. Rows describe **changes to an existing table**, not constraints declared when creating it. - -The combined SQL-runner column applies **individually to DbUp and Evolve**: both execute supplied SQL rather than diffing/rebuilding the schema. grate and RoundhousE follow the same distinction. Manual does not mean the engine cannot perform the operation. - -Evidence: [EF Core SQLite operation table][ef-sqlite], [FluentMigrator SQLite generator][f-sqlite-generator], [inherited SQL templates][f-generic-generator] and [processor][f-sqlite-processor], [DbUp scripts][d-usage], [Evolve concepts][e-concepts]. Migrator cells are supported by the source/test inventory below. - -| Existing-table operation | Migrator | FluentMigrator | EF Core | DbUp / Evolve | -| ---------------------------- | --------------------------------- | ------------------------------- | ----------- | ---------------------------------------------- | -| Add ordinary column | R | N | N | Manual SQL | -| Remove column | R | N; engine restrictions | R | Manual SQL/rebuild | -| Rename column | R | N; engine restrictions | N | Manual SQL/rebuild | -| Change declared type | R | Manual | R | Manual rebuild | -| Change nullability | R | Manual | R | Manual SQL on 3.53+ / rebuild on older engines | -| Change default | R via full `Column` | Manual | R via alter | Manual rebuild | -| Remove default | R via dedicated API; caveat below | Manual | R via alter | Manual rebuild | -| Add primary key | R | Manual | R | Manual rebuild | -| Remove primary key | R | Manual | R | Manual rebuild | -| Add foreign key | R | Manual | R | Manual rebuild | -| Remove foreign key | R | Manual | R | Manual rebuild | -| Add unique constraint | R | U | R | Manual rebuild/index | -| Remove unique constraint | R | U for tool-created unique index | R | Manual rebuild/index | -| Add check constraint | R | Manual | R | Manual rebuild | -| Remove check constraint | R | Manual | R | Manual rebuild | -| Create / drop ordinary index | N | N | N | Manual SQL | -| Rename table | N | N | N | Manual SQL | - -The table describes framework paths, not everything the newest SQLite engine can do. Migrator still rebuilds for nullability changes; FluentMigrator still rejects its general alter-column expression even when a newer engine can execute a hand-authored NOT NULL alteration. - -EF Core rebuilds rely on model-represented artifacts; the docs identify failures for artifacts outside that model. EF 9+ uses a SQLite lock table with abandoned-lock recovery considerations. These are separate from rebuild support. [SQLite limitations][ef-sqlite]. - -FluentMigrator supports inline FKs during table creation. Its reviewed generator directs callers to manual reconstruction for later FK changes; `LOOSE` mode skips unsupported expressions rather than emulating them. Unique-index substitution does not imply that an existing table-level UNIQUE constraint can be dropped as an index. [Generator][f-sqlite-generator]. - -### Migrator's emulated operations, precisely - -Methods refer to the pinned [SQLite provider][m-sqlite]. Tests illustrate evidence, not exhaustive coverage of every data/schema combination. - -| API / operation | Implementation behavior | Qualification / evidence | -| ------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `AddColumn` | Adds a column and mapping without an old source column; rebuilds. | Existing rows receive SQLite default/NULL behavior; incompatible NOT NULL requirements can fail. [Tests][t-add-column]. | -| `ChangeColumn` | Replaces the entire matching `Column` definition; rebuilds. | Specify properties to retain. Type affinity during copying is not arbitrary data conversion. [Tests][t-change-column]. | -| `RemoveColumnDefaultValue` | Clears parsed default; rebuilds. | Dedicated API is exercised, but generic `ChangeColumn_RemoveDefaultValue_Success` is skipped under issue #139. Not every default-removal path is verified. [Tests][t-sqlite-general]. | -| `RemoveColumn` | Removes column/mapping and matching single-column indexes/uniques/FKs; rebuilds affected tables. | Rejects detected CHECK references and composite dependencies until adjusted. Can remove inbound single-column FKs from other tables. [Tests][t-remove-column]. | -| `RenameColumn` | Changes copy mapping, column and represented key/index references; adjusts referencing tables. | Requires FK enforcement off; not an arbitrary SQL-expression rewriter. [Tests][t-rename-column]. | -| `AddPrimaryKey` | Sets membership, orders selected columns, rebuilds. | Composite keys supported; `PrimaryKeyExists` checks for any PK rather than matching its name. [Tests][t-pk]. | -| `RemovePrimaryKey` | Clears PK/PK-identity flags; rebuilds. | Changes identity-related semantics; review referencing tables. [Source][m-sqlite]. | -| `AddForeignKey` / `RemoveForeignKey` | Adds/removes represented FK; rebuilds child table. | Validate existing rows and enforcement. [FK tests][t-fk], [integrity tests][t-integrity]. | -| `AddUniqueConstraint` | Adds named unique definition; rebuilds. | Duplicate data can reject the copy. [Metadata tests][t-uniques]. | -| `AddCheckConstraint` | Adds named CHECK SQL; rebuilds. | Predicate must accept existing rows and be understood by the reader. [Tests][t-check]. | -| `RemoveConstraint` | Removes matching unique and check definitions; rebuilds. | Does not remove FKs/PKs; use dedicated APIs. [Source][m-sqlite]. | -| `RemoveAllConstraints` | Removes PK/unique definitions via rebuilds. | Retains FKs and leaves CHECK handling incomplete; not an all-constraint eraser. [Tests][t-remove-constraints], [source][m-sqlite]. | -| `RemoveAllIndexes` | Clears indexes **and unique constraints**; rebuilds. | Broader than dropping non-unique indexes. [Source][m-sqlite]. | -| `RecreateTable` | Public low-level schema/mapping reconstruction. | Requires a consistent supported representation. [Composite-key round-trip test][t-recreate]. | -| `TruncateTable` | Emits `DELETE FROM`. | Not native TRUNCATE and not an identity-sequence reset. [Source][m-sqlite]. | - -### What survives reconstruction—and what is not guaranteed - -| Schema/data detail | Migrator at the pinned revision | Implication | -| ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| Mapped rows | Named-column `INSERT … SELECT`. | New constraints/types must accept the data. | -| Names, parsed types, nullability, defaults | Included in column model. | Not a lossless representation of arbitrary CREATE SQL. | -| Composite PKs | Represented; dedicated rebuild test. | Check membership/order when replacing definitions. | -| FKs and delete actions | Read from schema/PRAGMA; emitted into replacement DDL. | Not a promise about every clause, e.g. arbitrary deferrability. | -| Unique / CHECK definitions | Included in `SQLiteTableInfo`. | Reader restrictions apply; rename does not rewrite arbitrary CHECK expressions. | -| Indexes / represented filters | Recreated after replacement. | Complex predicates, expressions, collations and sort details require separate verification. | -| Triggers | No trigger collection/replay in schema model or rebuild. | Do not assume preservation; a table drop removes its triggers. Recreate as needed. | -| Views / dependent SQL | No general dependency-SQL rewrite. | Validate/recreate dependencies after renames/drops. | -| `WITHOUT ROWID`, `STRICT`, generated columns | Not modeled as a complete round-trip contract. | No blanket preservation claim for external schemas. | -| Hidden `rowid` / AUTOINCREMENT high-water mark | Only mapped columns copied; no explicit sequence-state restoration. | Historical rowid/sequence metadata may change. | -| Type / length enforcement | Changes declarations, not SQLite typing rules. | Declared size is not SQL Server-like length enforcement. | -| FK enforcement state | Runner disables before migration and restores after successful execution. | Direct provider calls differ; exception restoration is not proven by success-path tests. | -| Whole-database FK validation | Integrity helper exists; runner does not automatically invoke it. | Enabling enforcement alone does not validate existing rows. | - -Evidence: [SQLite provider][m-sqlite], [schema model][m-sqlite-model], [execution][m-execution], [SQLite reconstruction procedure][sqlite-alter]. Re-evaluate these limitations after the parallel refactoring. - -### How the other frameworks compare on preservation - -| Framework | Replacement schema source | Responsibility for unsupported dependencies | -| ------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| Migrator | Live reader + `SQLiteTableInfo`. | Author handles objects outside the representation. | -| EF Core | Model/migration metadata. | Author handles artifacts outside automatic model-based rebuilding. [Docs][ef-sqlite]. | -| FluentMigrator | No general rebuild engine found in inspected SQLite components. | Author writes reconstruction for unsupported alterations. [Generator][f-sqlite-generator], [processor][f-sqlite-processor]. | -| DbUp | Project SQL / C#. | Script author. [Usage][d-usage]. | -| Evolve | Project SQL. | Script author. [Concepts][e-concepts]. | -| grate / RoundhousE | Project SQL. | Script author. Database integration is not emulation. [grate][g-home], [RoundhousE][r-home]. | -| EF6 | Selected provider's migration generator. | Provider-specific; EF Core rebuild support must not be attributed to EF6. No specific EF6 SQLite emulation verified here. | - -**Practical conclusion:** Migrator's differentiator is live-schema-based SQLite reconstruction without an ORM model. It is not unique in automatic SQLite rebuilding—EF Core also does this—and is not a lossless rewriter of every SQLite schema feature. - -## EF6, grate and RoundhousE - -Evidence: [EF6 migrations][ef6-main], [automatic migrations][ef6-auto], [history][ef6-history], [CLI][ef6-cli]; [grate home][g-home], [configuration][g-config], [script types][g-types], [anytime][g-anytime], [everytime][g-everytime], [one-time][g-onetime]; [RoundhousE][r-home] and [grate migration guide][g-migrate]. - -| Capability | EF6 Code First | grate | RoundhousE | -| ------------------------------------------- | --------------------------------------------- | --------------------------------- | ----------------------------------------------- | -| Authoring | C# from EF6 model | Lifecycle SQL folders | Lifecycle SQL folders | -| ORM dependency | EF6 model/context | None | None | -| Model-difference generation | Yes | No | No | -| Automatic migrations without explicit files | Optional EF6 feature | No | No | -| Reverse version | `Down`, target migration | Forward/custom recovery | Forward/custom recovery | -| Change once | Versioned migration | One-time scripts | One-time scripts | -| Run after content change | Not a SQL repeatable mechanism | Anytime scripts | Anytime workflow | -| Every deployment | Seed/custom lifecycle | Everytime scripts | Everytime workflow | -| Detect script edits | Not a script checksum journal | One-time hash checking | Changed-script policies | -| Existing-schema baseline | Existing-schema workflow | `--baseline` | Verify release's workflow | -| Transactions | EF/provider execution | Opt-in `--transaction` | Transaction flags / outside-transaction scripts | -| Environment filtering | Host/configuration | Filename conventions | Environment scripts | -| SQL token replacement | Custom | User tokens | Tokens | -| History separation | Context history / customization | Migration schema/configuration | Repository/schema conventions | -| Preview / inspection | Script generation | `--dryrun`, logs | Check release's dry-run/log tooling | -| Execution | PMC/runtime; `ef6.exe` replaces `migrate.exe` | CLI; self-contained distributions | CLI / .NET tooling | -| Automatic SQLite emulation | Provider-specific; not verified | None in documented workflow | None in documented workflow | - -RoundhousE maintainers point to grate as a successor. The migration guide documents differences; do not assume parity for every flag, history configuration or folder. This is a compatibility consideration, not a claim of identical release/support status. - -## Flyway and Liquibase in a .NET deployment - -These can migrate databases used by .NET applications, but do not replace Migrator's in-process C# transformation API directly. This narrower comparison avoids folding edition-dependent features into the main matrices. - -| Concern | Flyway | Liquibase | -| ------------------------- | ----------------------------------------------------------------- | --------------------------------------------------- | -| Artifacts | Versioned / repeatable migrations | Changelog changesets, including formatted SQL | -| Recovery | Explicit undo migrations where the selected edition supports Undo | Change-type-dependent / authored rollback | -| Selection and assumptions | Tool configuration; check command/edition | Contexts/preconditions; format/version restrictions | -| Automatic SQLite rebuild | Not established here; supplied SQL is not emulation | Not established here; verify change type/extension | -| .NET integration | Separate deployment tool | Separate deployment tool | - -Sources: [Flyway Undo][flyway-undo], [baseline migrations][flyway-baseline], [Liquibase rollback][liquibase-rollback], [preconditions][liquibase-preconditions]. This document does not claim that every command is available in a free edition. - -## Choosing a framework and identifying Migrator gaps - -These interpretations are grounded in the preceding evidence, rather than universal recommendations. - -| Requirement | Candidate / tradeoff | -| ---------------------------------------------- | ----------------------------------------------------------------------- | -| No ORM model, frequent SQLite alterations | Evaluate Migrator's live-schema reconstruction and preservation limits. | -| EF model defines schema | EF Core supplies scaffolding, rebuilds and deployment artifacts. | -| Handwritten C# / packaged runners / fluent DSL | FluentMigrator; manual work for unsupported SQLite alterations. | -| SQL-first runner composed in .NET | DbUp's script providers, journal and transaction strategies. | -| SQL checksums / change-triggered repeatables | Evolve's built-in conventions. | -| Existing RoundhousE folders | Evaluate grate's migration guide and history compatibility. | -| Existing EF6 application | Assess EF6/provider behavior separately from EF Core. | -| Multi-language database-owned deployment | Evaluate Flyway/Liquibase and required editions. | - -Potential Migrator improvements, **not implemented-feature claims**: - -1. A packaged CLI and dedicated SQL-preview/export workflow. -2. Validation of edits to already applied migration content. -3. Cross-process migration locking and explicit failure recovery. -4. Repeatable migrations distinct from execution hooks. -5. Stronger SQLite preservation of triggers, generated columns, table options and complex indexes. -6. Clearer bulk-removal semantics and FK-state restoration after exceptions. -7. Continued operation-level provider documentation and live test coverage. - -## Validation and maintenance - -Reviewed in a separate Git worktree based on `ab3aa9f`. No migration implementation files or parallel-refactoring checkout were changed. - -The existing SQLite category was executed on Windows: - -```sh -dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release --filter "TestCategory=SQLite" -``` - -**139 passed, 1 skipped, 0 failed.** The skipped test is `ChangeColumn_RemoveDefaultValue_Success`, documented by [issue #139](https://github.com/dotnetprojects/Migrator.NET/issues/139). Existing compiler warnings were present. This validates existing scenarios, not the complete preservation matrix. Competitors were reviewed through documentation/source, **not executed in a comparative test harness**. - -When updating: - -- Pin the new source revision and recheck SQLite rebuilds after refactoring. -- Verify competitor provider versions before promoting “Check” to a compatibility promise. -- Keep native SQL, automatic emulation and author-written workarounds distinct. -- Review ignored tests, schema round trips and real data, not just generated SQL. -- Update the date, sources and homepage summary together. - -## Source index - -- **Migrator:** [revision][m-revision], [runner][m-runner], [loader][m-loader], [execution][m-execution], [lifecycle][m-migration], [API][m-api], [history][m-provider], [factory][m-factory], [live tests][m-live], [SQLite implementation][m-sqlite], [SQLite model][m-sqlite-model]. -- **FluentMigrator:** [quick start][f-start], [configuration][f-config], [SQL][f-sql], [auto-reverse][f-reverse], [maintenance][f-maintenance], [profiles][f-profiles], pinned [SQLite generator][f-sqlite-generator] and [processor][f-sqlite-processor]. -- **EF Core:** [overview][ef-overview], [management][ef-managing], [deployment][ef-applying], [history][ef-history], [providers][ef-providers], [seeding][ef-seeding], [SQLite][ef-sqlite]. -- **DbUp:** [usage][d-usage], [providers][d-providers], [journal][d-journal], [script types][d-types], [transactions][d-transactions], [variables][d-variables], [logging][d-logging], [databases][d-databases], [philosophy][d-philosophy]. -- **Evolve:** [concepts][e-concepts], [options][e-options], [execution][e-start], [requirements][e-requirements]. -- **EF6:** [migrations][ef6-main], [automatic][ef6-auto], [history][ef6-history], [CLI][ef6-cli]. -- **grate / RoundhousE:** [grate][g-home], [options][g-config], [script types][g-types], [migration guide][g-migrate], [RoundhousE][r-home]. -- **SQLite engine:** [ALTER TABLE and reconstruction procedure][sqlite-alter]. - -[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/Migrator.cs -[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/MigrationLoader.cs -[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/MigrateAnywhere.cs -[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/Framework/Migration.cs -[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/Framework/ITransformationProvider.cs -[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/Providers/TransformationProvider.cs -[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/ProviderFactory.cs -[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/docs/live-database-tests.md -[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs -[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs -[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs -[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs -[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs -[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs -[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs -[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs -[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs -[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs -[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs -[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs -[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs -[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/ab3aa9f488196139334ae4b2ea335e803a280533/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs -[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/ab3aa9f488196139334ae4b2ea335e803a280533/ -[f-start]: https://fluentmigrator.github.io/intro/quick-start.html -[f-config]: https://fluentmigrator.github.io/intro/configuration.html -[f-sql]: https://fluentmigrator.github.io/operations/execute-sql.html -[f-reverse]: https://fluentmigrator.github.io/migration-types/auto-reversing.html -[f-maintenance]: https://fluentmigrator.github.io/migration-types/maintenance.html -[f-profiles]: https://fluentmigrator.github.io/migration-types/profiles.html -[f-sqlite-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteGenerator.cs -[f-sqlite-processor]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Processors/SQLite/SQLiteProcessor.cs -[ef-overview]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/ -[ef-managing]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing -[ef-applying]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying -[ef-history]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/history-table -[ef-providers]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers -[ef-seeding]: https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding -[ef-sqlite]: https://learn.microsoft.com/en-us/ef/core/providers/sqlite/limitations -[d-usage]: https://dbup.readthedocs.io/en/latest/usage/ -[d-providers]: https://dbup.readthedocs.io/en/latest/more-info/script-providers/ -[d-journal]: https://dbup.readthedocs.io/en/latest/more-info/journaling/ -[d-types]: https://dbup.readthedocs.io/en/latest/more-info/script-types/ -[d-transactions]: https://dbup.readthedocs.io/en/latest/more-info/transactions/ -[d-variables]: https://dbup.readthedocs.io/en/latest/more-info/variable-substitution/ -[d-logging]: https://dbup.readthedocs.io/en/latest/more-info/logging/ -[d-databases]: https://dbup.readthedocs.io/en/latest/supported-databases/ -[d-philosophy]: https://dbup.readthedocs.io/en/latest/philosophy-behind-dbup/ -[e-concepts]: https://evolve-db.netlify.app/concepts/ -[e-options]: https://evolve-db.netlify.app/configuration/options/ -[e-start]: https://evolve-db.netlify.app/getting-started/ -[e-requirements]: https://evolve-db.netlify.app/requirements/ -[ef6-main]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ -[ef6-auto]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/automatic -[ef6-history]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/history-customization -[ef6-cli]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ef6-exe -[g-home]: https://grate-devs.github.io/grate/ -[g-config]: https://grate-devs.github.io/grate/configuration-options/ -[g-types]: https://grate-devs.github.io/grate/script-types/ -[g-anytime]: https://grate-devs.github.io/grate/script-types/anytime/ -[g-everytime]: https://grate-devs.github.io/grate/script-types/everytime/ -[g-onetime]: https://grate-devs.github.io/grate/script-types/one-time/ -[g-migrate]: https://grate-devs.github.io/grate/migrating-from-roundhouse/ -[r-home]: https://github.com/chucknorris/roundhouse -[sqlite-alter]: https://www.sqlite.org/lang_altertable.html -[flyway-undo]: https://documentation.red-gate.com/flyway/reference/commands/undo -[flyway-baseline]: https://www.red-gate.com/hub/product-learning/flyway/flyways-baseline-migrations-explained-simply/ -[liquibase-rollback]: https://support.liquibase.com/hc/en-us/articles/29383086010523-How-to-Define-Rollbacks -[liquibase-preconditions]: https://docs.liquibase.com/community/user-guide-5-0-4/what-are-preconditions -[f-generic-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.Core/Generators/Generic/GenericGenerator.cs +# .NET database migration frameworks: detailed feature comparison + +**Reviewed: 22 September 2026.** This is a capability comparison, not a benchmark or an overall ranking. + +The main matrices cover **DotNetProjects.Migrator, FluentMigrator, EF Core migrations, DbUp and Evolve**—all five frameworks on the homepage. Additional sections cover **EF6, grate and RoundhousE**, with a short boundary comparison for **Flyway and Liquibase**. This is a defined shortlist, not a claim to catalogue every migration package ever published. + +Migrator findings are pinned to upgrade-stack commit [`874cb88`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. + +[Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Project README](../README.md) · [SQLite emulation comparison](#sqlite-emulation-comparison) · [Source index](#source-index) + +## Contents + +- [How to read the matrices](#how-to-read-the-matrices) +- [Authoring and application integration](#authoring-and-application-integration) +- [Schema and data operations](#schema-and-data-operations) +- [History, ordering and repeatability](#history-ordering-and-repeatability) +- [Transactions, rollback and coordination](#transactions-rollback-and-coordination) +- [Deployment, inspection and configuration](#deployment-inspection-and-configuration) +- [Database coverage and portability](#database-coverage-and-portability) +- [SQLite emulation comparison](#sqlite-emulation-comparison) +- [EF6, grate and RoundhousE](#ef6-grate-and-roundhouse) +- [Flyway and Liquibase in a .NET deployment](#flyway-and-liquibase-in-a-net-deployment) +- [Choosing a framework and identifying Migrator gaps](#choosing-a-framework-and-identifying-migrator-gaps) +- [Validation and maintenance](#validation-and-maintenance) +- [Source index](#source-index) + +## How to read the matrices + +| Term | Meaning | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Built-in / named API | The reviewed tool provides this operation or workflow. Database restrictions still apply. | +| Configure | Available through documented runner settings, composition or extension points. | +| Custom | You supply application code, SQL or deployment orchestration. Not automatic framework behavior. | +| No built-in | No implementation in the inspected Migrator source, or no equivalent in the reviewed documented workflow. It does not rule out third-party extensions. | +| Provider-dependent | Availability or semantics depend on the database integration and release. | +| Not verified | Evidence is insufficient for a positive or negative compatibility claim. | + +A SQL runner can execute a hand-authored table rebuild; that does **not** mean it automatically emulates `AlterColumn`. Likewise, recording applied migrations is not schema-drift detection, a transaction is not a deployment mutex, and a version downgrade is not a data restore. + +## Authoring and application integration + +Evidence: [Migrator runner][m-runner], [loader][m-loader], [migration contract][m-migration]; [FluentMigrator quick start][f-start] and [SQL execution][f-sql]; [EF Core overview][ef-overview] and [managing migrations][ef-managing]; [DbUp usage][d-usage] and [script providers][d-providers]; [Evolve concepts][e-concepts] and [configuration][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| ---------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------- | ------------------------------------- | ------------------------------------------ | +| Primary authoring artifact | Public C# migration class | C# migration class with fluent expressions | Generated, editable C# migration + model snapshot | SQL file or C# `IScript` | Versioned SQL file | +| Requires an ORM model | No | No | Yes, for normal scaffolding | No | No | +| Generates changes from model differences | No built-in | No built-in model differ in core workflow | Yes | No; author scripts | No; author scripts | +| Migration without a model change | Yes | Yes | Empty migration, then custom operations | Yes | Yes | +| Schema DSL / transformation API | Imperative API and structured `MigrationBuilder`; provider limits apply | Fluent create/alter/delete expressions | `MigrationBuilder` operations | No schema DSL; SQL / commands | No schema DSL; SQL | +| Custom C# logic | `Up` / `Down`; open provider | Migration code / connection operations | SQL/custom operations for database work | `IScript` and command factory | Surrounding host logic; migrations are SQL | +| Raw SQL | Command, query and scalar APIs | Inline, file and embedded SQL | `migrationBuilder.Sql` | Primary workflow | Primary workflow | +| Migration discovery | Assembly scan or explicit `Type[]` | Assembly scanning / filters | Context's migration assembly | Configurable script providers | Locations or embedded resources | +| Constructor dependency injection | Optional Microsoft DI/options package; custom activator supported | Runner/DI integration | Context services; migration customization is separate | Custom script provider/host if needed | No C# migration constructors | +| Embedded execution | Yes | Yes | Yes | Yes | Library mode | +| Dedicated execution host | Library or source-built packaged .NET tool (unreleased) | Library or packaged runner | Tooling, bundles or custom host | Write your own | CLI, .NET tool or library | + +EF Core's model snapshot comparison is not a live-database schema comparison. DbUp's C# support is more than static SQL file loading, but it does not supply a cross-database schema-operation layer. + +## Schema and data operations + +This table separates having an authoring API from that API working identically on every engine. SQLite is broken out below. Evidence: [Migrator interface][m-api] and [provider factory][m-factory]; [FluentMigrator operations][f-start]; [EF Core migration operations][ef-managing]; [DbUp script execution][d-usage]; [Evolve SQL model][e-concepts]. + +| Operation family | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| --------------------------------------- | ---------------------------------------- | ----------------------------------- | ------------------------------------------ | ----------------------------- | ------------------------------------- | +| Create / drop table | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | +| Rename table | Schema API | Fluent API | Migration operation | Author SQL | Author SQL | +| Add / drop / rename column | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | +| Change type / nullability / default | `ChangeColumn` and default API | Alter expressions | `AlterColumn` | Author SQL | Author SQL | +| Primary / composite keys | API; provider-dependent | Fluent expressions | Migration operations | Author SQL | Author SQL | +| Foreign keys / delete behavior | API; mapped constraint types | Fluent expressions | Migration operations | Author SQL | Author SQL | +| Unique constraints | API | Fluent expressions | Migration operations | Author SQL | Author SQL | +| Check constraints | API using SQL predicate | Provider/custom SQL as applicable | Migration operations | Author SQL | Author SQL | +| Indexes | API and index model | Fluent expressions | Operations / provider annotations | Author SQL | Author SQL | +| Filtered / included / clustered indexes | Provider-specific subsets | Provider-specific options | Provider-specific support | Engine-specific SQL | Engine-specific SQL | +| Views | `AddView` and SQL | Usually SQL | Usually SQL migrations | Author SQL | SQL; repeatables useful | +| Stored procedures / triggers | Raw SQL | SQL / connection operations | SQL / custom operations | Author SQL | Author SQL | +| Fixed-data insert / update / delete | Data API | Fluent data expressions | `InsertData` / `UpdateData` / `DeleteData` | SQL or C# | SQL | +| Transform existing data | SQL, provider reads/writes, copy helpers | SQL / connection operations | SQL / custom operations | SQL or C# | SQL | +| Live table / column existence | Existence and metadata APIs | Schema query API | SQL/custom code | SQL or C# | SQL | +| Full schema-drift report | No built-in | Not established by version tracking | Snapshot comparison alone is insufficient | Journal alone is insufficient | Checksums concern scripts, not schema | + +## History, ordering and repeatability + +Evidence: [Migrator loader][m-loader], [execution][m-execution] and [history storage][m-provider]; [FluentMigrator configuration][f-config], [maintenance][f-maintenance] and [profiles][f-profiles]; [EF Core overview][ef-overview], [history][ef-history] and [seeding][ef-seeding]; [DbUp journaling][d-journal], [script types][d-types] and [usage][d-usage]; [Evolve concepts][e-concepts] and [options][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| -------------------------- | ------------------------------------------------------- | -------------------------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------------- | +| Applied-change identity | Numeric version within scope | Migration version | Migration ID | Script name | Script metadata | +| Default history | `SchemaInfo` | Version table | `__EFMigrationsHistory` | E.g. `SchemaVersions` | `changelog` | +| History customization | Table name; scope column | Version-table metadata | Table/schema; custom services | Custom journal / table | Metadata table/schema | +| Independent modules | Scope + selected migrations | Separate history + filters | Contexts/assemblies + separate history | Filters + separate journals | Locations + separate metadata | +| Environment selection | Tags with explicit Any/All matching; scopes and named profiles | Tags / profiles / configuration | Context/deployment configuration | Filters / host | Locations / placeholders / host | +| Skip applied work | Version history | Version history | Migration history | Journal | Metadata | +| Applied-source checksum | No built-in | Not a core version-table guarantee | No script checksum journal | Standard journal tracks names; custom validation | Script checksums | +| Late lower-numbered change | Revisits missing versions up to target | Check runner policy | Do not assume IDs make diverging branches safe | Unrecorded scripts eligible; ordering matters | `OutOfOrder` | +| Repeat on content change | Custom | Not equivalent to maintenance/profiles | Not equivalent to seeding | Custom checksum-aware runner | Repeatable SQL | +| Always-run work | Ordered before/after-run and before/after-migration stages; selected profiles | Maintenance / selected profiles | Seeding APIs, EF 9+ | `RunAlways` / `NullJournal` | Not identical to RunAlways | +| Existing-schema baseline | Custom verified history initialization | Custom baseline/runner strategy | Existing-schema workflow | `MarkAsExecuted` | `StartVersion` / skip options | +| Repair checksums | Not applicable | Not established by version history | Not applicable | Custom journal concern | `repair` | + +**Migrator scope detail:** unscoped migrations inherit the runner scope. Explicitly scoped migrations are selected only for that scope; duplicate validation and history access use the same effective scope. Custom legacy providers without `IMigrationHistory` retain their prior behavior. History isolation is not table isolation. [Loader][m-loader], [execution][m-execution], [provider][m-provider]. + +## Transactions, rollback and coordination + +Evidence: [Migrator execution][m-execution] and [runner][m-runner]; [FluentMigrator configuration][f-config] and [auto-reverse][f-reverse]; [EF Core management][ef-managing], [deployment][ef-applying] and [SQLite limitations][ef-sqlite]; [DbUp transactions][d-transactions] and [philosophy][d-philosophy]; [Evolve concepts][e-concepts] and [options][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| ------------------------------ | -------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------ | +| Default transaction unit | Per migration | Per migration; configurable | Version-sensitive: EF 9 grouped pending migrations, reverted in EF 10 | None | Per migration | +| Whole-run transaction | `WholeSession` for verified SQLite, PostgreSQL and SQL Server dialects | Configure/orchestrate; check runner | Depends on version/operations | `WithTransaction()` | `CommitAll` | +| Per-change transaction opt-out | Run-level `None`; no per-migration transaction attribute | Transaction behavior | Raw SQL suppression | Choose strategy / separate runs | Script opt-out | +| Failed DDL rollback | Engine-dependent | Engine-dependent | Engine-dependent | When enabled and supported | Engine-dependent | +| Reverse committed migration | Authored `Down()` | `Down()` | Generated/editable `Down()` | Custom undo / forward fix | Forward fix; no Down command | +| Generate reverse operations | Supported create/rename operations; explicit reverse required for destructive/data/SQL operations | Supported auto-reverse expressions | Scaffolding; review output | No schema reverse generator | No | +| Target earlier version | `MigrateTo` | Down/rollback APIs | Earlier target / reverse script | Custom | Target limits forward work, not undo | +| Restore deleted data | Backup / reconstruction | Same | Same | Same | Same | +| Cross-process coordination | Opt-in native session locks for SQL Server, PostgreSQL and MySQL/MariaDB; custom abstraction | Serialize deployment / application-lock pattern | Migration locking, EF 9+; execution-path dependent | Host/provider concern; journal is not a lock | Cluster setting; provider-dependent | +| Post-commit hooks | `AfterUp` / `AfterDown` | Maintenance stages | Host/seeding lifecycle; not direct equivalent | Host / ordered scripts | Host / ordered scripts | + +A scope, checksum, history primary key or ordinary database write lock does not prove that two deployments can safely run the entire sequence concurrently. Evolve's cluster setting must be checked for the selected provider; it is not a blanket SQLite session-lock guarantee. + +## Deployment, inspection and configuration + +Evidence: [Migrator runner][m-runner] and [execution][m-execution]; [FluentMigrator runners][f-start] and [configuration][f-config]; [EF Core deployment][ef-applying]; [DbUp usage][d-usage], [variables][d-variables] and [logging][d-logging]; [Evolve execution][e-start] and [options][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| ------------------------------------ | ------------------------------------------------- | ------------------------------------------ | --------------------------------- | -------------------------------- | ------------------------------------------------ | +| Packaged CLI | Source project `DotNetProjects.Migrator.Tool`; not published by this upgrade | Yes | `dotnet ef` | Core library; custom host | Yes | +| Dedicated migration bundle generator | No; publish host | Package runner/migrations | Yes | Publish host | CLI distribution, not EF-style bundle generation | +| Review SQL without applying | Connected/offline structured subset; unsupported operations fail explicitly | Preview/output | Scripts | Authored SQL / pending scripts | Authored SQL | +| Dry-run qualification | `DryRun` plans versions without migration bodies, callbacks, transactions or history creation | Processor preview; user code needs care | Not a full side-effect simulation | Pending list / custom simulation | `RollbackAll` actually executes | +| Idempotent deployment SQL | Custom | Preview is not idempotent history guarding | Provider-dependent; not SQLite | Author SQL / use journal | Author SQL / use metadata | +| Status | Versions / loaded types | Runner/tool info | CLI / history APIs | Pending/executed APIs | `info` | +| Command timeout | Provider setting | Processor setting | Database/provider setting | Runner/provider setting | `CommandTimeout` | +| Logging | Legacy logger plus optional Microsoft logging adapter (SQL/exception details omitted) | Logging integration | EF logging | `IUpgradeLog` / integrations | Host/CLI | +| SQL substitution | Custom | Script tokens | Custom logic | `$variable$` | `${placeholder}` | +| Deployment identity | Host connection | Runner connection | Migration connection | Host connection | Tool connection | + +**Migrator dry run is not an offline SQL preview.** Execution starts provider work while `Up()`/`Down()` are skipped. It cannot show SQL from those skipped bodies and should not be described as side-effect-free database validation. [Execution source][m-execution]. + +## Database coverage and portability + +| Framework | How support is supplied | What it does not guarantee | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| Migrator | Source dialects + separate ADO.NET drivers. Live CI covers SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix and Sybase; Ingres is another source dialect. [CI guide][m-live]. | Every server/driver release, operation or arbitrary SQL construct. | +| FluentMigrator | Provider generators/processors. [Configuration][f-config]. | The same expression working on every engine. | +| EF Core | Relational provider packages. [Multiple providers][ef-providers]. | One provider's generated migrations working unchanged elsewhere. | +| DbUp | Database integrations. [Provider list][d-databases]. | SQL dialect translation. | +| Evolve | Database integrations. [Requirements][e-requirements]. | SQL translation or identical transactions. | + +A migration can compile yet require a table copy, lose an unsupported schema detail or fail on existing data. Compare the exact operation and data shape, not just database names. + +## SQLite emulation comparison + +### What emulation means + +SQLite has native table rename, column rename, add-column and (on sufficiently recent engines, subject to restrictions) drop-column operations. SQLite 3.53.0 added native `ALTER COLUMN … SET/DROP NOT NULL`; it still does not provide general type/default alteration or `ALTER TABLE ADD/DROP CONSTRAINT`. More complex changes require a replacement table, copying rows and rebuilding dependent objects. Native capabilities evolve independently of the .NET driver package. [SQLite ALTER TABLE reference][sqlite-alter]. + +Migrator reads the **live schema** into `SQLiteTableInfo`, modifies that representation and calls `RecreateTable`. It creates `
Temp`, copies mapped columns with `INSERT … SELECT`, drops the original, renames the replacement and recreates represented indexes. This works without an ORM model, but depends on what its schema reader can represent. [Implementation][m-sqlite], [schema model][m-sqlite-model]. + +### Automatic operation matrix + +**R** = built-in rebuild; **N** = native SQL path, subject to engine restrictions; **U** = unique-index substitution; **Manual** = author the change/rebuild yourself; **Manual** also covers a generated statement that the engine does not support. Rows describe **changes to an existing table**, not constraints declared when creating it. + +The combined SQL-runner column applies **individually to DbUp and Evolve**: both execute supplied SQL rather than diffing/rebuilding the schema. grate and RoundhousE follow the same distinction. Manual does not mean the engine cannot perform the operation. + +Evidence: [EF Core SQLite operation table][ef-sqlite], [FluentMigrator SQLite generator][f-sqlite-generator], [inherited SQL templates][f-generic-generator] and [processor][f-sqlite-processor], [DbUp scripts][d-usage], [Evolve concepts][e-concepts]. Migrator cells are supported by the source/test inventory below. + +| Existing-table operation | Migrator | FluentMigrator | EF Core | DbUp / Evolve | +| ---------------------------- | --------------------------------- | ------------------------------- | ----------- | ---------------------------------------------- | +| Add ordinary column | R | N | N | Manual SQL | +| Remove column | R | N; engine restrictions | R | Manual SQL/rebuild | +| Rename column | N on SQLite 3.26+; R fallback | N; engine restrictions | N | Manual SQL/rebuild | +| Change declared type | R | Manual | R | Manual rebuild | +| Change nullability | R | Manual | R | Manual SQL on 3.53+ / rebuild on older engines | +| Change default | R via full `Column` | Manual | R via alter | Manual rebuild | +| Remove default | R via dedicated API; caveat below | Manual | R via alter | Manual rebuild | +| Add primary key | R | Manual | R | Manual rebuild | +| Remove primary key | R | Manual | R | Manual rebuild | +| Add foreign key | R | Manual | R | Manual rebuild | +| Remove foreign key | R | Manual | R | Manual rebuild | +| Add unique constraint | R | U | R | Manual rebuild/index | +| Remove unique constraint | R | U for tool-created unique index | R | Manual rebuild/index | +| Add check constraint | R | Manual | R | Manual rebuild | +| Remove check constraint | R | Manual | R | Manual rebuild | +| Create / drop ordinary index | N | N | N | Manual SQL | +| Rename table | N | N | N | Manual SQL | + +The table describes framework paths, not everything the newest SQLite engine can do. Migrator still rebuilds for nullability changes; FluentMigrator still rejects its general alter-column expression even when a newer engine can execute a hand-authored NOT NULL alteration. + +EF Core rebuilds rely on model-represented artifacts; the docs identify failures for artifacts outside that model. EF 9+ uses a SQLite lock table with abandoned-lock recovery considerations. These are separate from rebuild support. [SQLite limitations][ef-sqlite]. + +FluentMigrator supports inline FKs during table creation. Its reviewed generator directs callers to manual reconstruction for later FK changes; `LOOSE` mode skips unsupported expressions rather than emulating them. Unique-index substitution does not imply that an existing table-level UNIQUE constraint can be dropped as an index. [Generator][f-sqlite-generator]. + +### Migrator's emulated operations, precisely + +Methods refer to the pinned [SQLite provider][m-sqlite]. Tests illustrate evidence, not exhaustive coverage of every data/schema combination. + +| API / operation | Implementation behavior | Qualification / evidence | +| ------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `AddColumn` | Adds a column and mapping without an old source column; rebuilds. | Existing rows receive SQLite default/NULL behavior; incompatible NOT NULL requirements can fail. [Tests][t-add-column]. | +| `ChangeColumn` | Replaces the entire matching `Column` definition; rebuilds. | Specify properties to retain. Type affinity during copying is not arbitrary data conversion. [Tests][t-change-column]. | +| `RemoveColumnDefaultValue` | Clears parsed default; rebuilds. Generic default-removal regression is enabled and passes. | Dedicated and generic default-removal regressions run; provider CI is required for changes. [Tests][t-sqlite-general]. | +| `RemoveColumn` | Removes column/mapping and matching single-column indexes/uniques/FKs; rebuilds affected tables. | Rejects detected CHECK references and composite dependencies until adjusted. Can remove inbound single-column FKs from other tables. [Tests][t-remove-column]. | +| `RenameColumn` | Native on SQLite 3.26+; reconstruction fallback for older engines. | Native rename delegates dependency rewriting to SQLite; reconstruction is not an arbitrary SQL-expression rewriter. [Tests][t-rename-column]. | +| `AddPrimaryKey` | Sets membership, orders selected columns, rebuilds. | Composite keys supported; `PrimaryKeyExists` checks for any PK rather than matching its name. [Tests][t-pk]. | +| `RemovePrimaryKey` | Clears PK/PK-identity flags; rebuilds. | Changes identity-related semantics; review referencing tables. [Source][m-sqlite]. | +| `AddForeignKey` / `RemoveForeignKey` | Adds/removes represented FK; rebuilds child table. | Validate existing rows and enforcement. [FK tests][t-fk], [integrity tests][t-integrity]. | +| `AddUniqueConstraint` | Adds named unique definition; rebuilds. | Duplicate data can reject the copy. [Metadata tests][t-uniques]. | +| `AddCheckConstraint` | Adds named CHECK SQL; rebuilds. | Predicate must accept existing rows and be understood by the reader. [Tests][t-check]. | +| `RemoveConstraint` | Removes matching unique and check definitions; rebuilds. | Does not remove FKs/PKs; use dedicated APIs. [Source][m-sqlite]. | +| `RemoveAllConstraints` | Clears PK, unique, FK and CHECK definitions before rebuilding. | Constraint removal can fail when dependent schemas/data require a coordinated migration. [Tests][t-remove-constraints], [source][m-sqlite]. | +| `RemoveAllIndexes` | Clears indexes **and unique constraints**; rebuilds. | Broader than dropping non-unique indexes. [Source][m-sqlite]. | +| `RecreateTable` | Public low-level schema/mapping reconstruction. | Requires a consistent supported representation. [Composite-key round-trip test][t-recreate]. | +| `TruncateTable` | Emits `DELETE FROM`. | Not native TRUNCATE and not an identity-sequence reset. [Source][m-sqlite]. | + +### What survives reconstruction—and what is not guaranteed + +| Schema/data detail | Migrator at the pinned revision | Implication | +| ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| Mapped rows | Named-column `INSERT … SELECT`. | New constraints/types must accept the data. | +| Names, parsed types, nullability, defaults | Included in column model. | Not a lossless representation of arbitrary CREATE SQL. | +| Composite PKs | Represented; dedicated rebuild test. | Check membership/order when replacing definitions. | +| FKs and delete actions | Read from schema/PRAGMA; emitted into replacement DDL. | Not a promise about every clause, e.g. arbitrary deferrability. | +| Unique / CHECK definitions | Included in `SQLiteTableInfo`. | Reader restrictions apply; rename does not rewrite arbitrary CHECK expressions. | +| Indexes / represented filters | Recreated after replacement. | Complex predicates, expressions, collations and sort details require separate verification. | +| Triggers | Collected and replayed for supported rebuilds without renames; unsafe rename fallback rejected. | Trigger SQL is replayed only where the rebuild does not require rewriting its identifiers. | +| Views / dependent SQL | No general dependency-SQL rewrite. | Validate/recreate dependencies after renames/drops. | +| `WITHOUT ROWID`, `STRICT`, generated columns | Unsupported reconstruction is rejected before dropping the original. | No preservation claim for unsupported external table properties. | +| Hidden `rowid` / AUTOINCREMENT high-water mark | Only mapped columns copied; no explicit sequence-state restoration. | Historical rowid/sequence metadata may change. | +| Type / length enforcement | Changes declarations, not SQLite typing rules. | Declared size is not SQL Server-like length enforcement. | +| FK enforcement state | Runner and owned rebuild transactions restore the prior setting after success/failure. | Caller-owned active SQLite transactions require FK settings to be configured before beginning the transaction. | +| Whole-database FK validation | Runner and owned rebuild transactions validate integrity before commit. | Enabling enforcement alone does not validate existing rows. | + +Evidence: [SQLite provider][m-sqlite], [schema model][m-sqlite-model], [execution][m-execution], [SQLite reconstruction procedure][sqlite-alter]. Native drop-column selection, sequence high-water preservation and arbitrary dependency rewriting remain gaps. + +### How the other frameworks compare on preservation + +| Framework | Replacement schema source | Responsibility for unsupported dependencies | +| ------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| Migrator | Live reader + `SQLiteTableInfo`. | Author handles objects outside the representation. | +| EF Core | Model/migration metadata. | Author handles artifacts outside automatic model-based rebuilding. [Docs][ef-sqlite]. | +| FluentMigrator | No general rebuild engine found in inspected SQLite components. | Author writes reconstruction for unsupported alterations. [Generator][f-sqlite-generator], [processor][f-sqlite-processor]. | +| DbUp | Project SQL / C#. | Script author. [Usage][d-usage]. | +| Evolve | Project SQL. | Script author. [Concepts][e-concepts]. | +| grate / RoundhousE | Project SQL. | Script author. Database integration is not emulation. [grate][g-home], [RoundhousE][r-home]. | +| EF6 | Selected provider's migration generator. | Provider-specific; EF Core rebuild support must not be attributed to EF6. No specific EF6 SQLite emulation verified here. | + +**Practical conclusion:** Migrator's differentiator is live-schema-based SQLite reconstruction without an ORM model. It is not unique in automatic SQLite rebuilding—EF Core also does this—and is not a lossless rewriter of every SQLite schema feature. + +## EF6, grate and RoundhousE + +Evidence: [EF6 migrations][ef6-main], [automatic migrations][ef6-auto], [history][ef6-history], [CLI][ef6-cli]; [grate home][g-home], [configuration][g-config], [script types][g-types], [anytime][g-anytime], [everytime][g-everytime], [one-time][g-onetime]; [RoundhousE][r-home] and [grate migration guide][g-migrate]. + +| Capability | EF6 Code First | grate | RoundhousE | +| ------------------------------------------- | --------------------------------------------- | --------------------------------- | ----------------------------------------------- | +| Authoring | C# from EF6 model | Lifecycle SQL folders | Lifecycle SQL folders | +| ORM dependency | EF6 model/context | None | None | +| Model-difference generation | Yes | No | No | +| Automatic migrations without explicit files | Optional EF6 feature | No | No | +| Reverse version | `Down`, target migration | Forward/custom recovery | Forward/custom recovery | +| Change once | Versioned migration | One-time scripts | One-time scripts | +| Run after content change | Not a SQL repeatable mechanism | Anytime scripts | Anytime workflow | +| Every deployment | Seed/custom lifecycle | Everytime scripts | Everytime workflow | +| Detect script edits | Not a script checksum journal | One-time hash checking | Changed-script policies | +| Existing-schema baseline | Existing-schema workflow | `--baseline` | Verify release's workflow | +| Transactions | EF/provider execution | Opt-in `--transaction` | Transaction flags / outside-transaction scripts | +| Environment filtering | Host/configuration | Filename conventions | Environment scripts | +| SQL token replacement | Custom | User tokens | Tokens | +| History separation | Context history / customization | Migration schema/configuration | Repository/schema conventions | +| Preview / inspection | Script generation | `--dryrun`, logs | Check release's dry-run/log tooling | +| Execution | PMC/runtime; `ef6.exe` replaces `migrate.exe` | CLI; self-contained distributions | CLI / .NET tooling | +| Automatic SQLite emulation | Provider-specific; not verified | None in documented workflow | None in documented workflow | + +RoundhousE maintainers point to grate as a successor. The migration guide documents differences; do not assume parity for every flag, history configuration or folder. This is a compatibility consideration, not a claim of identical release/support status. + +## Flyway and Liquibase in a .NET deployment + +These can migrate databases used by .NET applications, but do not replace Migrator's in-process C# transformation API directly. This narrower comparison avoids folding edition-dependent features into the main matrices. + +| Concern | Flyway | Liquibase | +| ------------------------- | ----------------------------------------------------------------- | --------------------------------------------------- | +| Artifacts | Versioned / repeatable migrations | Changelog changesets, including formatted SQL | +| Recovery | Explicit undo migrations where the selected edition supports Undo | Change-type-dependent / authored rollback | +| Selection and assumptions | Tool configuration; check command/edition | Contexts/preconditions; format/version restrictions | +| Automatic SQLite rebuild | Not established here; supplied SQL is not emulation | Not established here; verify change type/extension | +| .NET integration | Separate deployment tool | Separate deployment tool | + +Sources: [Flyway Undo][flyway-undo], [baseline migrations][flyway-baseline], [Liquibase rollback][liquibase-rollback], [preconditions][liquibase-preconditions]. This document does not claim that every command is available in a free edition. + +## Choosing a framework and identifying Migrator gaps + +These interpretations are grounded in the preceding evidence, rather than universal recommendations. + +| Requirement | Candidate / tradeoff | +| ---------------------------------------------- | ----------------------------------------------------------------------- | +| No ORM model, frequent SQLite alterations | Evaluate Migrator's live-schema reconstruction and preservation limits. | +| EF model defines schema | EF Core supplies scaffolding, rebuilds and deployment artifacts. | +| Handwritten C# / packaged runners / fluent DSL | FluentMigrator; manual work for unsupported SQLite alterations. | +| SQL-first runner composed in .NET | DbUp's script providers, journal and transaction strategies. | +| SQL checksums / change-triggered repeatables | Evolve's built-in conventions. | +| Existing RoundhousE folders | Evaluate grate's migration guide and history compatibility. | +| Existing EF6 application | Assess EF6/provider behavior separately from EF Core. | +| Multi-language database-owned deployment | Evaluate Flyway/Liquibase and required editions. | + +Potential Migrator improvements, **not implemented-feature claims**: + +1. Broader structured SQL-preview coverage, provider-specific batch scripts and CLI deployment validation. The source CLI and preview subset already exist. +2. Validation of edits to already applied migration content. +3. More native lock backends and recovery/concurrency validation; three database families now have opt-in locks. +4. Repeatable migrations distinct from execution hooks. +5. SQLite native drop-column selection, generated columns, table options, sequence state and complex-index preservation beyond the currently guarded subset. +6. Complete imperative/fluent operation coverage and ownership-aware default/uniqueness cleanup. +7. Continued operation-level provider documentation and live test coverage. + +## Validation and maintenance + +The original master baseline (`ab3aa9f`) had 139 passing SQLite tests and one skipped default-removal test. At upgrade source `874cb88`, a rebuilt solution passed **83 unit tests and 160 SQLite tests, with no skips**. Provider PR #174 passed all eleven database/unit jobs and the coverage gate in [run 35729926918](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35729926918). New native-lock tests in #177 require their own live CI verification. + +This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; batch scripts, operation inventory, several provider ownership/metadata fixes and broader deployment regressions remain work in progress. Competitors were reviewed through documentation/source, **not executed in a comparative harness**. + +When updating: + +- Pin the new source revision and recheck SQLite rebuilds after refactoring. +- Verify competitor provider versions before promoting “Check” to a compatibility promise. +- Keep native SQL, automatic emulation and author-written workarounds distinct. +- Review ignored tests, schema round trips and real data, not just generated SQL. +- Update the date, sources and homepage summary together. + +## Source index + +- **Migrator:** [revision][m-revision], [runner][m-runner], [loader][m-loader], [execution][m-execution], [lifecycle][m-migration], [API][m-api], [history][m-provider], [factory][m-factory], [live tests][m-live], [SQLite implementation][m-sqlite], [SQLite model][m-sqlite-model]. +- **FluentMigrator:** [quick start][f-start], [configuration][f-config], [SQL][f-sql], [auto-reverse][f-reverse], [maintenance][f-maintenance], [profiles][f-profiles], pinned [SQLite generator][f-sqlite-generator] and [processor][f-sqlite-processor]. +- **EF Core:** [overview][ef-overview], [management][ef-managing], [deployment][ef-applying], [history][ef-history], [providers][ef-providers], [seeding][ef-seeding], [SQLite][ef-sqlite]. +- **DbUp:** [usage][d-usage], [providers][d-providers], [journal][d-journal], [script types][d-types], [transactions][d-transactions], [variables][d-variables], [logging][d-logging], [databases][d-databases], [philosophy][d-philosophy]. +- **Evolve:** [concepts][e-concepts], [options][e-options], [execution][e-start], [requirements][e-requirements]. +- **EF6:** [migrations][ef6-main], [automatic][ef6-auto], [history][ef6-history], [CLI][ef6-cli]. +- **grate / RoundhousE:** [grate][g-home], [options][g-config], [script types][g-types], [migration guide][g-migrate], [RoundhousE][r-home]. +- **SQLite engine:** [ALTER TABLE and reconstruction procedure][sqlite-alter]. + +[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Migrator.cs +[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/MigrationLoader.cs +[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/MigrationExecution.cs +[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Framework/Migration.cs +[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Framework/ITransformationProvider.cs +[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Providers/TransformationProvider.cs +[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/ProviderFactory.cs +[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/docs/live-database-tests.md +[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs +[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/874cb88c43a2134baa7a5355145ec16809b8c350/ +[f-start]: https://fluentmigrator.github.io/intro/quick-start.html +[f-config]: https://fluentmigrator.github.io/intro/configuration.html +[f-sql]: https://fluentmigrator.github.io/operations/execute-sql.html +[f-reverse]: https://fluentmigrator.github.io/migration-types/auto-reversing.html +[f-maintenance]: https://fluentmigrator.github.io/migration-types/maintenance.html +[f-profiles]: https://fluentmigrator.github.io/migration-types/profiles.html +[f-sqlite-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteGenerator.cs +[f-sqlite-processor]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Processors/SQLite/SQLiteProcessor.cs +[ef-overview]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/ +[ef-managing]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing +[ef-applying]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying +[ef-history]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/history-table +[ef-providers]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers +[ef-seeding]: https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding +[ef-sqlite]: https://learn.microsoft.com/en-us/ef/core/providers/sqlite/limitations +[d-usage]: https://dbup.readthedocs.io/en/latest/usage/ +[d-providers]: https://dbup.readthedocs.io/en/latest/more-info/script-providers/ +[d-journal]: https://dbup.readthedocs.io/en/latest/more-info/journaling/ +[d-types]: https://dbup.readthedocs.io/en/latest/more-info/script-types/ +[d-transactions]: https://dbup.readthedocs.io/en/latest/more-info/transactions/ +[d-variables]: https://dbup.readthedocs.io/en/latest/more-info/variable-substitution/ +[d-logging]: https://dbup.readthedocs.io/en/latest/more-info/logging/ +[d-databases]: https://dbup.readthedocs.io/en/latest/supported-databases/ +[d-philosophy]: https://dbup.readthedocs.io/en/latest/philosophy-behind-dbup/ +[e-concepts]: https://evolve-db.netlify.app/concepts/ +[e-options]: https://evolve-db.netlify.app/configuration/options/ +[e-start]: https://evolve-db.netlify.app/getting-started/ +[e-requirements]: https://evolve-db.netlify.app/requirements/ +[ef6-main]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ +[ef6-auto]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/automatic +[ef6-history]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/history-customization +[ef6-cli]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ef6-exe +[g-home]: https://grate-devs.github.io/grate/ +[g-config]: https://grate-devs.github.io/grate/configuration-options/ +[g-types]: https://grate-devs.github.io/grate/script-types/ +[g-anytime]: https://grate-devs.github.io/grate/script-types/anytime/ +[g-everytime]: https://grate-devs.github.io/grate/script-types/everytime/ +[g-onetime]: https://grate-devs.github.io/grate/script-types/one-time/ +[g-migrate]: https://grate-devs.github.io/grate/migrating-from-roundhouse/ +[r-home]: https://github.com/chucknorris/roundhouse +[sqlite-alter]: https://www.sqlite.org/lang_altertable.html +[flyway-undo]: https://documentation.red-gate.com/flyway/reference/commands/undo +[flyway-baseline]: https://www.red-gate.com/hub/product-learning/flyway/flyways-baseline-migrations-explained-simply/ +[liquibase-rollback]: https://support.liquibase.com/hc/en-us/articles/29383086010523-How-to-Define-Rollbacks +[liquibase-preconditions]: https://docs.liquibase.com/community/user-guide-5-0-4/what-are-preconditions +[f-generic-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.Core/Generators/Generic/GenericGenerator.cs diff --git a/docs/runner-guide.md b/docs/runner-guide.md new file mode 100644 index 00000000..8c7c423a --- /dev/null +++ b/docs/runner-guide.md @@ -0,0 +1,100 @@ +# Runner and fluent API upgrade + +These APIs describe the source upgrade under review in PRs #173, #174, #175 and #177. They are not a statement about the currently released NuGet packages. Build the repository to try them; no package publication is part of this change. + +## Fluent quick start + +The [compiled quick-start project](../examples/FluentQuickStart/Program.cs) executes preview, migration and automatic reversal against SQLite: + +```sh +dotnet run --project examples/FluentQuickStart +``` + +```csharp +[Migration(1, Scope = "demo"), Tags("core")] +public class CreateUsers : AutoReversingMigration +{ + public override void BuildUp(MigrationBuilder migration) + { + migration.Create.Table("Users") + .WithColumn("Id").AsInt32().PrimaryKey() + .WithColumn("Name").AsString(255).NotNullable(); + } +} +``` + +Use `DotNetProjects.Migrator`, `.Framework` and `.Framework.Fluent`. A table definition is completed before execution. Existing imperative `Migration.Up/Down` classes keep working. `FluentMigration` supports authored `BuildDown`; `AutoReversingMigration` reverses supported create/rename operations in reverse order. Destructive changes, data, SQL and callbacks need explicit reverse operations. Automatic reversal never restores deleted data. + +The builder has `Create`, `Alter`, `Delete`, `Rename`, `Insert`, `Update`, `Execute` and `Administration`. Schema inspection is exposed through `FluentMigration.Schema`, and the provider through `Context`. History and transaction methods remain explicit context operations. Some administrative/data-copy operations use provider callbacks and cannot generate SQL previews. + +## Runner options + +`runner.Options` supports: + +| Option | Semantics | +| --- | --- | +| `Tags` / `TagMatch` | Ordinal names; explicit `Any` or `All`. No filter selects all versioned migrations. Filtered applied versions remain applied on downgrade. | +| `Profiles` | Explicit names of `[Profile("name")]` classes. Run after versioned migrations without recording versions; run again when selected again. | +| `TransactionMode` | `PerMigration` by default; `None` or `WholeSession` available. | +| `Activator` | Optional constructor activation delegate. | +| `Lock` / `LockTimeout` | Optional `IMigrationLock` lease; acquire before reading history and release on completion/failure. | + +Unscoped migrations inherit the provider scope; explicitly scoped migrations run only in that scope. Discovery, duplicate validation and history reads use the effective scope. Scopes separate history, not tables. Legacy custom providers can adopt the additive `IMigrationHistory` interface for read-only planning and effective-scope selection. + +Maintenance classes use `[Maintenance(MaintenanceStage.BeforeRun)]`, `BeforeMigration`, `AfterMigration` or `AfterRun`. Profiles and maintenance accept `Order` and `Scope`. Ordering uses `Order` then ordinal full type name. Hooks stop on failure; later hooks are not cleanup guarantees. Connection/transaction restoration and lock release do not depend on hooks running. Profiles and maintenance use `Up`; they do not acquire version records. + +## Transactions and locks + +`PerMigration` commits each successful migration. `None` leaves transaction behavior to the provider/operations. `WholeSession` is accepted for SQLite, PostgreSQL and SQL Server dialects; history-table initialization occurs before that transaction. Other dialects fail explicitly because transactional DDL has not been verified. Arbitrary imperative SQL can still violate transaction assumptions; database administration and implicit-commit statements require separate runs. + +`AfterUp`/`AfterDown` run after commit. In whole-session mode they are deferred until the complete session commits. Their failure reports an error after durable changes; it cannot undo a successful commit. Caller-owned connections remain caller-owned. + +`new DatabaseMigrationLock()` uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. Locks are session-owned, keyed by database/history table/scope, and remain held across migration commits. Do not switch databases, replace/close the connection or manipulate the native lock inside a migration. Unsupported providers, including SQLite, reject this lock implementation. Supply a custom `IMigrationLock` where another coordination mechanism is required. MySQL named locks coordinate one server, not an entire distributed cluster. + +## Planning and SQL preview + +`runner.Plan(target)` and `DryRun` inspect history without creating/upgrading it and do not invoke migration bodies, callbacks, transactions or SQLite PRAGMA changes. Custom providers must implement `IMigrationHistory` for these paths. + +`runner.PreviewSql(target, providerType)` connects for history/schema reads. `MigrationSqlPreview.Generate(providerType, migrations)` can generate SQL offline. Earlier structured operations update a planned schema so later operations can refer to newly created/renamed tables. SQL preview currently supports a subset: basic tables/columns, supported renames, simple indexes, inserts and raw SQL. Unsupported alterations, constraints, filters, callbacks and schema dependencies fail explicitly. Output is operation SQL, not an idempotent history-managed deployment bundle. + +Imperative bodies require `allowLegacyBodies: true`. Provider calls are captured through a rejecting proxy: direct connections, commands and unsupported reads/callbacks are blocked. **Arbitrary C# cannot be sandboxed**: constructors, fluent authoring and opted-in imperative bodies can still access files, networks or external state. Use trusted migration code. `InitializeOnce` and post-commit callbacks do not run during preview. + +## CLI from source + +```sh +dotnet pack src/Migrator.Tool -o artifacts/packages +dotnet tool install DotNetProjects.Migrator.Tool --add-source artifacts/packages --tool-path artifacts/tools +``` + +Set `MIGRATOR_CONNECTION` in your environment; the tool does not print its value. Common commands: + +```sh +migrator list --assembly MyMigrations.dll --provider SQLite +migrator status --assembly MyMigrations.dll --provider SQLite +migrator validate --assembly MyMigrations.dll --provider SQLite +migrator plan --assembly MyMigrations.dll --provider SQLite --target 10 +migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql +migrator sql --assembly MyMigrations.dll --provider SQLite --offline --output migration.sql +migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession +migrator rollback --assembly MyMigrations.dll --provider SQLite --target 0 +``` + +Use `--connection-env NAME`, `--schema`, `--tags a,b`, `--tag-match Any|All`, `--profiles a,b`, `--timeout SECONDS`, `--lock` and `--lock-timeout SECONDS` where applicable. `rollback` requires an explicit target. Offline SQL assumes empty history and currently rejects profiles/maintenance. `validate` validates version planning, not arbitrary migration-body behavior. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird. Other library providers need a custom host. + +Exit codes: `0` success, `1` execution/load failure, `2` invalid arguments, `3` unsupported operation/provider, `4` lock timeout. SQL output may contain migration data; exception and provider trace details are omitted from CLI diagnostics. + +## Optional DI and logging + +The source package `DotNetProjects.Migrator.Extensions.DependencyInjection` provides `services.AddMigrator(providerFactory, migrationAssembly, configureOptions)`. Resolve `Migrator` inside a service scope; migration constructors use that scope's services. Options are scoped snapshots. Provider disposal follows the DI scope. Microsoft logging records lifecycle events while omitting SQL text and raw exception messages; the core retains its lightweight logger API. + +## Validation + +Build before using the test scripts (they intentionally use `--no-build`): + +```sh +dotnet build Migrator.slnx +pwsh .github/scripts/test.ps1 -Database Unit +pwsh .github/scripts/test.ps1 -Database SQLite +``` + +See [live database tests](live-database-tests.md) for the full matrix. Provider-specific changes need live provider evidence. Check PR CI and review threads after every push; reply with implementation/test evidence and resolve fixed findings. Keep commits descriptive and merge the PR stack in dependency order only after review. diff --git a/examples/FluentQuickStart/FluentQuickStart.csproj b/examples/FluentQuickStart/FluentQuickStart.csproj new file mode 100644 index 00000000..d0d7285c --- /dev/null +++ b/examples/FluentQuickStart/FluentQuickStart.csproj @@ -0,0 +1,4 @@ + + Exenet9.0enable + + diff --git a/examples/FluentQuickStart/Program.cs b/examples/FluentQuickStart/Program.cs new file mode 100644 index 00000000..b9e2ba48 --- /dev/null +++ b/examples/FluentQuickStart/Program.cs @@ -0,0 +1,30 @@ +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Fluent; +using DotNetProjects.Migrator.Providers; +using Microsoft.Data.Sqlite; + +using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True"); +connection.Open(); +using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null, "demo"); +var runner = new Migrator(provider, false, typeof(CreateUsers)); +runner.Options.Tags.Add("core"); +runner.Options.TransactionMode = MigrationTransactionMode.WholeSession; +Console.WriteLine(runner.PreviewSql(1, ProviderTypes.SQLite)); +if (provider.TableExists("Users") || provider.TableExists(provider.SchemaInfoTable)) throw new Exception("Preview wrote to the database."); +runner.MigrateToLastVersion(); +if (!provider.ColumnExists("Users", "Name")) throw new Exception("Migration failed."); +runner.MigrateTo(0); +if (provider.TableExists("Users")) throw new Exception("Automatic reversal failed."); +Console.WriteLine("Quick-start migration, preview and reversal passed."); + +[Migration(1, Scope = "demo"), Tags("core")] +public class CreateUsers : AutoReversingMigration +{ + public override void BuildUp(MigrationBuilder migration) + { + migration.Create.Table("Users") + .WithColumn("Id").AsInt32().PrimaryKey() + .WithColumn("Name").AsString(255).NotNullable(); + } +} From aec29abf7e84c2afbad2297b36acfc90034ffafb Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 15:43:12 +0200 Subject: [PATCH 12/17] Refresh framework comparison against the reviewed upgrade source Pin Migrator claims to 8d8818e and distinguish under-review source APIs from released packages. Document native SQLite column removal, retained autoincrement high-water state, typed fluent coverage and its validation limits. Explain initialization preview rejection and down-only rollback. Record exact local and live CI evidence without attributing earlier results to later commits. Normalize documentation line endings to avoid unrelated diff noise. Validation: solution build and compiled FluentQuickStart preview/migration/reversal passed; homepage previously inspected at desktop and mobile sizes. --- .gitignore | 40 +- README.md | 634 +++++------ docs/index.html | 1408 ++++++++++++------------ docs/migration-framework-comparison.md | 820 +++++++------- docs/runner-guide.md | 200 ++-- 5 files changed, 1551 insertions(+), 1551 deletions(-) diff --git a/.gitignore b/.gitignore index 02f80861..921f9fe6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,20 @@ -bin/ -obj/ -*.log -logs/ -_ReSharper*/ -output/ -release/ -*.suo -*.user -*.cache -packages/ - -.vs/ - -/src/GlobalAssemblyInfo.cs -*.gpState - -**/appsettings.Development.json -TestResults/ -/artifacts/ +bin/ +obj/ +*.log +logs/ +_ReSharper*/ +output/ +release/ +*.suo +*.user +*.cache +packages/ + +.vs/ + +/src/GlobalAssemblyInfo.cs +*.gpState + +**/appsettings.Development.json +TestResults/ +/artifacts/ diff --git a/README.md b/README.md index 529ea7a0..ac0fa223 100644 --- a/README.md +++ b/README.md @@ -1,317 +1,317 @@ -# DotNetProjects.Migrator - -**Versioned database migrations in C#, independent of your ORM.** - -[![NuGet version](https://img.shields.io/nuget/v/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) -[![NuGet downloads](https://img.shields.io/nuget/dt/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) -[![Build and tests](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml) -[![GitHub Pages](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml) -[![Source target: .NET 9](https://img.shields.io/badge/source_target-.NET_9-512BD4)](src/Migrator/DotNetProjects.Migrator.csproj) -[![License: MPL-1.1](https://img.shields.io/badge/license-MPL--1.1-blue.svg)](https://www.mozilla.org/en-US/MPL/1.1/) - -[Homepage & documentation](https://dotnetprojects.github.io/Migrator.NET/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) - -DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratordotnet/Migrator.NET). Write each schema change as a numbered C# class, commit it alongside your application, and use the runner to bring a database to the required version. The database records which migrations have already been applied. - -## Contents - -- [Why use it?](#why-use-it) -- [Installation and requirements](#installation-and-requirements) -- [Quick start](#quick-start) -- [Migration versions and rollback](#migration-versions-and-rollback) -- [Multiple modules and migration scopes](#multiple-modules-and-migration-scopes) -- [Schema and data operations](#schema-and-data-operations) -- [Database providers](#database-providers) -- [Comparison with other .NET frameworks](#comparison-with-other-net-frameworks) -- [Building and testing](#building-and-testing) -- [Documentation and GitHub Pages](#documentation-and-github-pages) -- [Contributing and project history](#contributing-and-project-history) -- [License](#license) - -## Why use it? - -- **Explicit C# migrations.** Define forward and reverse changes with `Up()` and `Down()`; review them like application code. -- **No ORM dependency.** Use it alongside EF, Dapper, another data layer, or plain ADO.NET. -- **Database transformation API.** Work with tables, columns, keys, indexes and data, with raw SQL available for provider-specific operations. -- **Version tracking.** Apply pending migrations or target a specific version using database-backed history. -- **Scoped histories.** Track multiple modules in one database when each runner is given the appropriate migration set. -- **Bring your database driver.** The library does not directly reference database-driver packages; supply an ADO.NET connection or configure the driver factory. -- **SQLite schema handling.** This fork includes schema inspection and table-recreation logic for operations SQLite cannot perform directly. - -The source upgrade adds a structured fluent API, runner filtering/lifecycle options, SQL-preview subset, native locking, a CLI project and optional Microsoft DI/logging integration. These changes are under review and **are not a released NuGet feature claim**. See the [runner and fluent guide](docs/runner-guide.md) and [detailed framework comparison](docs/migration-framework-comparison.md). EF-style model scaffolding and migration-content checksums remain outside the implementation. - -## Installation and requirements - -```sh -dotnet add package DotNetProjects.Migrator -``` - -Install the ADO.NET driver for your database separately. For the SQLite example below: - -```sh -dotnet add package Microsoft.Data.Sqlite --version 9.0.7 -``` - -The **current source targets `net9.0`**. Check the [NuGet package's framework list](https://www.nuget.org/packages/DotNetProjects.Migrator/#supportedframeworks-body-tab) for the particular release you install; older package releases may target different frameworks. The SQLite driver version above matches the repository's test dependency. - -Building the `.slnx` solution requires an SDK that understands that format, such as .NET SDK 9.0.200 or later. The runtime required by the current source is .NET 9. - -## Quick start - -### 1. Create a migration host - -```sh -dotnet new console -n MigrationDemo -f net9.0 -cd MigrationDemo -dotnet add package DotNetProjects.Migrator -dotnet add package Microsoft.Data.Sqlite --version 9.0.7 -``` - -### 2. Add `CreateUsers.cs` - -Migrations must be public classes implementing the migration contract, decorated with `[Migration(version)]`. Each version must be unique within the set loaded by one runner. - -```csharp -using System.Data; -using DotNetProjects.Migrator.Framework; - -[Migration(1)] -public class CreateUsers : Migration -{ - public override void Up() - { - Database.AddTable("Users", - new Column("Id", DbType.Int32, ColumnProperty.NotNull), - new Column("Name", DbType.String, 255)); - Database.AddPrimaryKey("PK_Users", "Users", "Id"); - } - - public override void Down() - { - Database.RemoveTable("Users"); - } -} -``` - -### 3. Replace `Program.cs` - -```csharp -using DotNetProjects.Migrator; -using DotNetProjects.Migrator.Providers; -using Microsoft.Data.Sqlite; - -using var connection = new SqliteConnection("Data Source=app.db"); -connection.Open(); - -using var provider = ProviderFactory.Create( - ProviderTypes.SQLite, connection, defaultSchema: null); - -var migrator = new Migrator( - provider, typeof(CreateUsers).Assembly, trace: false); - -if (migrator.LastAppliedMigrationVersion is long applied - && applied > migrator.AssemblyLastMigrationVersion) -{ - throw new InvalidOperationException( - "Database version is newer than this application."); -} - -migrator.MigrateToLastVersion(); -``` - -### 4. Run it - -```sh -dotnet run -``` - -This creates a local SQLite database containing `Users` and the migration history table. Running the application again skips version `1` because it has already been recorded. Add a new class with `[Migration(2)]` for the next change. - -The example supplies an **open** `IDbConnection`. The caller owns that connection and disposes it after the provider. If you use the connection-string overload instead, the selected provider must be able to resolve the appropriate ADO.NET factory. - -## Migration versions and rollback - -Use increasing numeric versions, or the attribute's date-based constructor: - -```csharp -[Migration(2026, 9, 22, 12, 0, 0)] -``` - -Keep applied migration classes in source control. Change the schema with a new migration instead of editing an already applied one: history records the version, not a checksum of the migration's content. - -| API | Purpose | -| ------------------------------ | -------------------------------------------------------------------------------- | -| `MigrateToLastVersion()` | Apply through the latest version in the loaded migration set. | -| `MigrateTo(version)` | Move to a chosen version, invoking `Up()` or `Down()` as required. | -| `AppliedMigrations` | List the versions recorded for the provider's scope. | -| `LastAppliedMigrationVersion` | Highest applied version, or `null` when none are applied. | -| `AssemblyLastMigrationVersion` | Highest version in the loaded migration set. | -| `SchemaInfoTableName` | Customize the history table name before accessing history or running migrations. | - -With the runner above, `migrator.MigrateTo(0)` reverses all applied migrations in its set. In this example that drops `Users`, including its data. A `Down()` implementation is a reverse schema operation, not a backup restore. - -Migration execution starts a transaction for each migration and attempts rollback on failure. Actual atomicity depends on the database, driver and operation; some databases implicitly commit DDL. `AfterUp()` and `AfterDown()` run **after commit**, so a failure in those hooks cannot undo the committed migration. - -For deployment, run a dedicated migration host before the application needs the new schema. Coordinate it so competing instances do not migrate the same database concurrently. Review and test both directions against your actual database engine. - -## Multiple modules and migration scopes - -The default history table is `SchemaInfo`, with version, scope and timestamp information. The default scope is `"default"`. You can use separate scopes for modules sharing a database. - -Within a host with an open `connection`, select the module's migration types explicitly: - -```csharp -using var billingProvider = ProviderFactory.Create( - ProviderTypes.SQLite, - connection, - defaultSchema: null, - scope: "billing"); - -var billingMigrator = new Migrator( - billingProvider, - false, - typeof(Billing001), - typeof(Billing002)); - -billingMigrator.MigrateToLastVersion(); -``` - -`Billing001` and `Billing002` represent your own public migration classes. Alternatively, give the runner an assembly that contains only that module's migrations. - -Important details: - -- 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). - -## Fluent API and deployment tooling - -Run the [compiled fluent example](examples/FluentQuickStart/Program.cs): - -```sh -dotnet run --project examples/FluentQuickStart -``` - -The example creates a complete table definition, previews it without changing history, runs a whole-session migration, then verifies automatic reversal. The [runner guide](docs/runner-guide.md) covers CLI commands, tags/profiles, maintenance, transactions, optional DI/logging, locks and preview limitations. Build the source packages locally to try the new tooling; no NuGet publication accompanies these PRs. - -## Schema and data operations - -Inside a migration, `Database` implements [`ITransformationProvider`](src/Migrator/Framework/ITransformationProvider.cs). It includes: - -| Area | Examples | -| ------------------ | ------------------------------------------------------------------------------------- | -| Tables and columns | `AddTable`, `RemoveTable`, `RenameTable`, `AddColumn`, `ChangeColumn`, `RemoveColumn` | -| Keys and indexes | `AddPrimaryKey`, `AddForeignKey`, `AddIndex` and corresponding removal operations | -| Schema inspection | `TableExists`, `ColumnExists`, `GetTables`, `GetColumns` | -| Data and SQL | `Insert`, `Update`, `Delete`, `ExecuteNonQuery`, `ExecuteQuery`, `ExecuteScalar` | - -For example, a new migration can add a column: - -```csharp -public override void Up() -{ - Database.AddColumn("Users", new Column("Email", DbType.String, 320)); -} - -public override void Down() -{ - Database.RemoveColumn("Users", "Email"); -} -``` - -Provider implementations determine which operations are available and how they map to SQL. Use `Database.ExecuteNonQuery(...)` for custom SQL and keep dialect-specific statements explicit. The source also includes a [schema builder API](src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs). - -## Database providers - -The [provider factory](src/Migrator/ProviderFactory.cs) contains these database families: - -| Database | `ProviderTypes` value(s) | -| ------------ | ---------------------------- | -| SQL Server | `SqlServer`, `SqlServer2005` | -| PostgreSQL | `PostgreSQL`, `PostgreSQL82` | -| SQLite | `SQLite`, `MonoSQLite` | -| MySQL | `Mysql` | -| MariaDB | `MariaDB` | -| Oracle | `Oracle`, `MsOracle` | -| IBM Db2 | `IBM_DB2` | -| IBM Informix | `IBM_Informix` | -| Firebird | `Firebird` | -| Ingres | `Ingres` | -| Sybase | `Sybase` | - -This is an inventory of dialects present in source, **not a guarantee that every server version, driver or operation is supported**. Some entries are legacy variants. Verify the combination you deploy against the [provider implementations](src/Migrator/Providers/Impl) and [provider tests](src/Migrator.Tests/Providers). - -## Comparison with other .NET frameworks - -Reviewed **22 September 2026**. Migrator's column describes this repository; the alternatives summarize their official documentation. These are workflow differences, not performance benchmarks or a ranking. - -| Capability | Migrator.NET (this fork) | FluentMigrator | EF Core | DbUp | Evolve | -| ---------------------------- | --------------------------------- | -------------------------------------------- | ------------------------------------ | -------------------------- | --------------------------------- | -| Authoring | Handwritten C# transformation API | Handwritten C# fluent DSL | C# scaffolded from model differences | SQL or C# scripts | Versioned SQL files | -| ORM-independent workflow | Yes | Yes | Uses EF model / DbContext | Yes | Yes | -| Model-difference scaffolding | No built-in generator | Hand-authored | Yes, with model snapshots | Hand-authored | Hand-authored | -| Downgrade applied migrations | Authored `Down()` | `Down()`; supported auto-reverse expressions | Generated/editable `Down()` | Custom undo or forward fix | Forward fix; no Down command | -| Separate histories | Scope + selected assembly/types | Custom version table + filtering | Contexts + custom history table | Journals + script filters | Metadata table/schema + locations | -| Execution | Library / custom host | Library + CLI | CLI, scripts, bundles, runtime | Library / custom host | Library, .NET tool, CLI | -| Recurring work | Custom code | Maintenance migrations / profiles | Seeding APIs (EF 9+) | `RunAlways` scripts | Checksum-based repeatable SQL | - -All five can execute raw SQL. Transaction support depends on database capabilities: Migrator starts one per migration; DbUp makes transactions opt-in; the others have configurable transaction behavior. Reversing a completed migration is different from rolling back a failed transaction. Evolve's checksum-based repeatables also differ from always-run scripts or lifecycle hooks. - -- Choose **Migrator** for direct C# schema operations, scoped history and integration with your own host. -- Consider **FluentMigrator** for its fluent authoring API, packaged runners, tags and profiles. -- Consider **EF Core migrations** when your EF model drives the schema and you want scaffolding and deployment artifacts. -- Consider **DbUp** for a SQL-oriented runner composed in .NET, or **Evolve** for convention-based SQL with checksum validation and repeatables. - -Sources: [Migrator runner](src/Migrator/Migrator.cs), [FluentMigrator quick start](https://fluentmigrator.github.io/intro/quick-start.html) and [configuration](https://fluentmigrator.github.io/intro/configuration.html), [EF Core migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) and [deployment](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying), [DbUp documentation](https://dbup.readthedocs.io/en/latest/) and [script types](https://dbup.readthedocs.io/en/latest/more-info/script-types/), [Evolve concepts](https://evolve-db.netlify.app/concepts/). The [full homepage comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) includes transaction, provider and source details; its source is available in [docs/index.html](docs/index.html). - -## Building and testing - -```sh -dotnet restore Migrator.slnx -dotnet build Migrator.slnx --configuration Release --no-restore -``` - -Tests use NUnit. Run a focused runner test fixture without provisioning external databases: - -```sh -dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release --filter "FullyQualifiedName~Migrator.Tests.MigratorTest" -``` - -The full suite includes database integration tests: - -```sh -dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release -``` - -Use disposable test databases: integration tests create, alter and remove schema objects. Configure connections in `src/Migrator.Tests/appsettings.Development.json` using the structure and identifiers in [appsettings.json](src/Migrator.Tests/appsettings.json), and set `ASPNETCORE_ENVIRONMENT=Development`. The development settings file is gitignored; keep credentials there rather than committing them. - -The [.NET workflow](.github/workflows/dotnetpull.yml) documents CI database services and commands. Provider coverage varies; a passing build alone does not validate every supported database family. - -### Live database testing - -See [live database testing](docs/live-database-tests.md) for the CI matrix, pinned versions, local commands, coverage, engine limitations and excluded candidates. - -## Documentation and GitHub Pages - -The homepage in [`docs/`](docs/README.md) includes installation, a runnable quick start, provider information and a sourced feature comparison. It uses plain HTML, CSS and JavaScript with no build dependencies. - -Preview locally from the repository root: - -```sh -python -m http.server 8766 --directory docs --bind 127.0.0.1 -``` - -Open [localhost:8766](http://localhost:8766). To publish, select **GitHub Actions** under **Settings → Pages → Build and deployment**, then merge the site into `master`. The [Pages workflow](.github/workflows/pages.yml) deploys changes to `docs/` at [dotnetprojects.github.io/Migrator.NET](https://dotnetprojects.github.io/Migrator.NET/). The workflow can also be dispatched manually on `master`. - -## Contributing and project history - -Bug reports, provider fixes, tests and documentation improvements are welcome through [issues](https://github.com/dotnetprojects/Migrator.NET/issues) and [pull requests](https://github.com/dotnetprojects/Migrator.NET/pulls). Include the package version, database/driver versions, a minimal migration that reproduces the problem, and expected versus actual behavior. Add a focused regression test for a behavior change and run the relevant provider tests. - -This project continues the original [Migrator.NET](https://github.com/migratordotnet/Migrator.NET), which began on Google Code. This fork incorporates contributions from other forks and work on SQLite schema reading and recreation, composite primary keys, SQL Server index inspection, reserved identifiers, provider independence and migration scopes. - -## License - -The package declares **Mozilla Public License 1.1 (MPL-1.1)** in its [project metadata](src/Migrator/DotNetProjects.Migrator.csproj). See the [license text](https://www.mozilla.org/en-US/MPL/1.1/) and source-file notices. +# DotNetProjects.Migrator + +**Versioned database migrations in C#, independent of your ORM.** + +[![NuGet version](https://img.shields.io/nuget/v/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) +[![NuGet downloads](https://img.shields.io/nuget/dt/DotNetProjects.Migrator.svg)](https://www.nuget.org/packages/DotNetProjects.Migrator/) +[![Build and tests](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/dotnetpull.yml) +[![GitHub Pages](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml/badge.svg?branch=master)](https://github.com/dotnetprojects/Migrator.NET/actions/workflows/pages.yml) +[![Source target: .NET 9](https://img.shields.io/badge/source_target-.NET_9-512BD4)](src/Migrator/DotNetProjects.Migrator.csproj) +[![License: MPL-1.1](https://img.shields.io/badge/license-MPL--1.1-blue.svg)](https://www.mozilla.org/en-US/MPL/1.1/) + +[Homepage & documentation](https://dotnetprojects.github.io/Migrator.NET/) · [NuGet](https://www.nuget.org/packages/DotNetProjects.Migrator/) · [Releases](https://github.com/dotnetprojects/Migrator.NET/releases) · [Issues](https://github.com/dotnetprojects/Migrator.NET/issues) · [Feature comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) + +DotNetProjects.Migrator is a fork of [Migrator.NET](https://github.com/migratordotnet/Migrator.NET). Write each schema change as a numbered C# class, commit it alongside your application, and use the runner to bring a database to the required version. The database records which migrations have already been applied. + +## Contents + +- [Why use it?](#why-use-it) +- [Installation and requirements](#installation-and-requirements) +- [Quick start](#quick-start) +- [Migration versions and rollback](#migration-versions-and-rollback) +- [Multiple modules and migration scopes](#multiple-modules-and-migration-scopes) +- [Schema and data operations](#schema-and-data-operations) +- [Database providers](#database-providers) +- [Comparison with other .NET frameworks](#comparison-with-other-net-frameworks) +- [Building and testing](#building-and-testing) +- [Documentation and GitHub Pages](#documentation-and-github-pages) +- [Contributing and project history](#contributing-and-project-history) +- [License](#license) + +## Why use it? + +- **Explicit C# migrations.** Define forward and reverse changes with `Up()` and `Down()`; review them like application code. +- **No ORM dependency.** Use it alongside EF, Dapper, another data layer, or plain ADO.NET. +- **Database transformation API.** Work with tables, columns, keys, indexes and data, with raw SQL available for provider-specific operations. +- **Version tracking.** Apply pending migrations or target a specific version using database-backed history. +- **Scoped histories.** Track multiple modules in one database when each runner is given the appropriate migration set. +- **Bring your database driver.** The library does not directly reference database-driver packages; supply an ADO.NET connection or configure the driver factory. +- **SQLite schema handling.** This fork includes schema inspection and table-recreation logic for operations SQLite cannot perform directly. + +The source upgrade adds a structured fluent API, runner filtering/lifecycle options, SQL-preview subset, native locking, a CLI project and optional Microsoft DI/logging integration. These changes are under review and **are not a released NuGet feature claim**. See the [runner and fluent guide](docs/runner-guide.md) and [detailed framework comparison](docs/migration-framework-comparison.md). EF-style model scaffolding and migration-content checksums remain outside the implementation. + +## Installation and requirements + +```sh +dotnet add package DotNetProjects.Migrator +``` + +Install the ADO.NET driver for your database separately. For the SQLite example below: + +```sh +dotnet add package Microsoft.Data.Sqlite --version 9.0.7 +``` + +The **current source targets `net9.0`**. Check the [NuGet package's framework list](https://www.nuget.org/packages/DotNetProjects.Migrator/#supportedframeworks-body-tab) for the particular release you install; older package releases may target different frameworks. The SQLite driver version above matches the repository's test dependency. + +Building the `.slnx` solution requires an SDK that understands that format, such as .NET SDK 9.0.200 or later. The runtime required by the current source is .NET 9. + +## Quick start + +### 1. Create a migration host + +```sh +dotnet new console -n MigrationDemo -f net9.0 +cd MigrationDemo +dotnet add package DotNetProjects.Migrator +dotnet add package Microsoft.Data.Sqlite --version 9.0.7 +``` + +### 2. Add `CreateUsers.cs` + +Migrations must be public classes implementing the migration contract, decorated with `[Migration(version)]`. Each version must be unique within the set loaded by one runner. + +```csharp +using System.Data; +using DotNetProjects.Migrator.Framework; + +[Migration(1)] +public class CreateUsers : Migration +{ + public override void Up() + { + Database.AddTable("Users", + new Column("Id", DbType.Int32, ColumnProperty.NotNull), + new Column("Name", DbType.String, 255)); + Database.AddPrimaryKey("PK_Users", "Users", "Id"); + } + + public override void Down() + { + Database.RemoveTable("Users"); + } +} +``` + +### 3. Replace `Program.cs` + +```csharp +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Providers; +using Microsoft.Data.Sqlite; + +using var connection = new SqliteConnection("Data Source=app.db"); +connection.Open(); + +using var provider = ProviderFactory.Create( + ProviderTypes.SQLite, connection, defaultSchema: null); + +var migrator = new Migrator( + provider, typeof(CreateUsers).Assembly, trace: false); + +if (migrator.LastAppliedMigrationVersion is long applied + && applied > migrator.AssemblyLastMigrationVersion) +{ + throw new InvalidOperationException( + "Database version is newer than this application."); +} + +migrator.MigrateToLastVersion(); +``` + +### 4. Run it + +```sh +dotnet run +``` + +This creates a local SQLite database containing `Users` and the migration history table. Running the application again skips version `1` because it has already been recorded. Add a new class with `[Migration(2)]` for the next change. + +The example supplies an **open** `IDbConnection`. The caller owns that connection and disposes it after the provider. If you use the connection-string overload instead, the selected provider must be able to resolve the appropriate ADO.NET factory. + +## Migration versions and rollback + +Use increasing numeric versions, or the attribute's date-based constructor: + +```csharp +[Migration(2026, 9, 22, 12, 0, 0)] +``` + +Keep applied migration classes in source control. Change the schema with a new migration instead of editing an already applied one: history records the version, not a checksum of the migration's content. + +| API | Purpose | +| ------------------------------ | -------------------------------------------------------------------------------- | +| `MigrateToLastVersion()` | Apply through the latest version in the loaded migration set. | +| `MigrateTo(version)` | Move to a chosen version, invoking `Up()` or `Down()` as required. | +| `AppliedMigrations` | List the versions recorded for the provider's scope. | +| `LastAppliedMigrationVersion` | Highest applied version, or `null` when none are applied. | +| `AssemblyLastMigrationVersion` | Highest version in the loaded migration set. | +| `SchemaInfoTableName` | Customize the history table name before accessing history or running migrations. | + +With the runner above, `migrator.MigrateTo(0)` reverses all applied migrations in its set. In this example that drops `Users`, including its data. A `Down()` implementation is a reverse schema operation, not a backup restore. + +Migration execution starts a transaction for each migration and attempts rollback on failure. Actual atomicity depends on the database, driver and operation; some databases implicitly commit DDL. `AfterUp()` and `AfterDown()` run **after commit**, so a failure in those hooks cannot undo the committed migration. + +For deployment, run a dedicated migration host before the application needs the new schema. Coordinate it so competing instances do not migrate the same database concurrently. Review and test both directions against your actual database engine. + +## Multiple modules and migration scopes + +The default history table is `SchemaInfo`, with version, scope and timestamp information. The default scope is `"default"`. You can use separate scopes for modules sharing a database. + +Within a host with an open `connection`, select the module's migration types explicitly: + +```csharp +using var billingProvider = ProviderFactory.Create( + ProviderTypes.SQLite, + connection, + defaultSchema: null, + scope: "billing"); + +var billingMigrator = new Migrator( + billingProvider, + false, + typeof(Billing001), + typeof(Billing002)); + +billingMigrator.MigrateToLastVersion(); +``` + +`Billing001` and `Billing002` represent your own public migration classes. Alternatively, give the runner an assembly that contains only that module's migrations. + +Important details: + +- 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). + +## Fluent API and deployment tooling + +Run the [compiled fluent example](examples/FluentQuickStart/Program.cs): + +```sh +dotnet run --project examples/FluentQuickStart +``` + +The example creates a complete table definition, previews it without changing history, runs a whole-session migration, then verifies automatic reversal. The [runner guide](docs/runner-guide.md) covers CLI commands, tags/profiles, maintenance, transactions, optional DI/logging, locks and preview limitations. Build the source packages locally to try the new tooling; no NuGet publication accompanies these PRs. + +## Schema and data operations + +Inside a migration, `Database` implements [`ITransformationProvider`](src/Migrator/Framework/ITransformationProvider.cs). It includes: + +| Area | Examples | +| ------------------ | ------------------------------------------------------------------------------------- | +| Tables and columns | `AddTable`, `RemoveTable`, `RenameTable`, `AddColumn`, `ChangeColumn`, `RemoveColumn` | +| Keys and indexes | `AddPrimaryKey`, `AddForeignKey`, `AddIndex` and corresponding removal operations | +| Schema inspection | `TableExists`, `ColumnExists`, `GetTables`, `GetColumns` | +| Data and SQL | `Insert`, `Update`, `Delete`, `ExecuteNonQuery`, `ExecuteQuery`, `ExecuteScalar` | + +For example, a new migration can add a column: + +```csharp +public override void Up() +{ + Database.AddColumn("Users", new Column("Email", DbType.String, 320)); +} + +public override void Down() +{ + Database.RemoveColumn("Users", "Email"); +} +``` + +Provider implementations determine which operations are available and how they map to SQL. Use `Database.ExecuteNonQuery(...)` for custom SQL and keep dialect-specific statements explicit. The source also includes a [schema builder API](src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs). + +## Database providers + +The [provider factory](src/Migrator/ProviderFactory.cs) contains these database families: + +| Database | `ProviderTypes` value(s) | +| ------------ | ---------------------------- | +| SQL Server | `SqlServer`, `SqlServer2005` | +| PostgreSQL | `PostgreSQL`, `PostgreSQL82` | +| SQLite | `SQLite`, `MonoSQLite` | +| MySQL | `Mysql` | +| MariaDB | `MariaDB` | +| Oracle | `Oracle`, `MsOracle` | +| IBM Db2 | `IBM_DB2` | +| IBM Informix | `IBM_Informix` | +| Firebird | `Firebird` | +| Ingres | `Ingres` | +| Sybase | `Sybase` | + +This is an inventory of dialects present in source, **not a guarantee that every server version, driver or operation is supported**. Some entries are legacy variants. Verify the combination you deploy against the [provider implementations](src/Migrator/Providers/Impl) and [provider tests](src/Migrator.Tests/Providers). + +## Comparison with other .NET frameworks + +Reviewed **22 September 2026**. Migrator's column describes this repository; the alternatives summarize their official documentation. These are workflow differences, not performance benchmarks or a ranking. + +| Capability | Migrator.NET (this fork) | FluentMigrator | EF Core | DbUp | Evolve | +| ---------------------------- | --------------------------------- | -------------------------------------------- | ------------------------------------ | -------------------------- | --------------------------------- | +| Authoring | Handwritten C# transformation API | Handwritten C# fluent DSL | C# scaffolded from model differences | SQL or C# scripts | Versioned SQL files | +| ORM-independent workflow | Yes | Yes | Uses EF model / DbContext | Yes | Yes | +| Model-difference scaffolding | No built-in generator | Hand-authored | Yes, with model snapshots | Hand-authored | Hand-authored | +| Downgrade applied migrations | Authored `Down()` | `Down()`; supported auto-reverse expressions | Generated/editable `Down()` | Custom undo or forward fix | Forward fix; no Down command | +| Separate histories | Scope + selected assembly/types | Custom version table + filtering | Contexts + custom history table | Journals + script filters | Metadata table/schema + locations | +| Execution | Library / custom host | Library + CLI | CLI, scripts, bundles, runtime | Library / custom host | Library, .NET tool, CLI | +| Recurring work | Custom code | Maintenance migrations / profiles | Seeding APIs (EF 9+) | `RunAlways` scripts | Checksum-based repeatable SQL | + +All five can execute raw SQL. Transaction support depends on database capabilities: Migrator starts one per migration; DbUp makes transactions opt-in; the others have configurable transaction behavior. Reversing a completed migration is different from rolling back a failed transaction. Evolve's checksum-based repeatables also differ from always-run scripts or lifecycle hooks. + +- Choose **Migrator** for direct C# schema operations, scoped history and integration with your own host. +- Consider **FluentMigrator** for its fluent authoring API, packaged runners, tags and profiles. +- Consider **EF Core migrations** when your EF model drives the schema and you want scaffolding and deployment artifacts. +- Consider **DbUp** for a SQL-oriented runner composed in .NET, or **Evolve** for convention-based SQL with checksum validation and repeatables. + +Sources: [Migrator runner](src/Migrator/Migrator.cs), [FluentMigrator quick start](https://fluentmigrator.github.io/intro/quick-start.html) and [configuration](https://fluentmigrator.github.io/intro/configuration.html), [EF Core migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) and [deployment](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying), [DbUp documentation](https://dbup.readthedocs.io/en/latest/) and [script types](https://dbup.readthedocs.io/en/latest/more-info/script-types/), [Evolve concepts](https://evolve-db.netlify.app/concepts/). The [full homepage comparison](https://dotnetprojects.github.io/Migrator.NET/#compare) includes transaction, provider and source details; its source is available in [docs/index.html](docs/index.html). + +## Building and testing + +```sh +dotnet restore Migrator.slnx +dotnet build Migrator.slnx --configuration Release --no-restore +``` + +Tests use NUnit. Run a focused runner test fixture without provisioning external databases: + +```sh +dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release --filter "FullyQualifiedName~Migrator.Tests.MigratorTest" +``` + +The full suite includes database integration tests: + +```sh +dotnet test src/Migrator.Tests/Migrator.Tests.csproj --configuration Release +``` + +Use disposable test databases: integration tests create, alter and remove schema objects. Configure connections in `src/Migrator.Tests/appsettings.Development.json` using the structure and identifiers in [appsettings.json](src/Migrator.Tests/appsettings.json), and set `ASPNETCORE_ENVIRONMENT=Development`. The development settings file is gitignored; keep credentials there rather than committing them. + +The [.NET workflow](.github/workflows/dotnetpull.yml) documents CI database services and commands. Provider coverage varies; a passing build alone does not validate every supported database family. + +### Live database testing + +See [live database testing](docs/live-database-tests.md) for the CI matrix, pinned versions, local commands, coverage, engine limitations and excluded candidates. + +## Documentation and GitHub Pages + +The homepage in [`docs/`](docs/README.md) includes installation, a runnable quick start, provider information and a sourced feature comparison. It uses plain HTML, CSS and JavaScript with no build dependencies. + +Preview locally from the repository root: + +```sh +python -m http.server 8766 --directory docs --bind 127.0.0.1 +``` + +Open [localhost:8766](http://localhost:8766). To publish, select **GitHub Actions** under **Settings → Pages → Build and deployment**, then merge the site into `master`. The [Pages workflow](.github/workflows/pages.yml) deploys changes to `docs/` at [dotnetprojects.github.io/Migrator.NET](https://dotnetprojects.github.io/Migrator.NET/). The workflow can also be dispatched manually on `master`. + +## Contributing and project history + +Bug reports, provider fixes, tests and documentation improvements are welcome through [issues](https://github.com/dotnetprojects/Migrator.NET/issues) and [pull requests](https://github.com/dotnetprojects/Migrator.NET/pulls). Include the package version, database/driver versions, a minimal migration that reproduces the problem, and expected versus actual behavior. Add a focused regression test for a behavior change and run the relevant provider tests. + +This project continues the original [Migrator.NET](https://github.com/migratordotnet/Migrator.NET), which began on Google Code. This fork incorporates contributions from other forks and work on SQLite schema reading and recreation, composite primary keys, SQL Server index inspection, reserved identifiers, provider independence and migration scopes. + +## License + +The package declares **Mozilla Public License 1.1 (MPL-1.1)** in its [project metadata](src/Migrator/DotNetProjects.Migrator.csproj). See the [license text](https://www.mozilla.org/en-US/MPL/1.1/) and source-file notices. diff --git a/docs/index.html b/docs/index.html index 99cccd77..09171875 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,704 +1,704 @@ - - - - - - - - Migrator.NET — Database changes, in your code. - - - - - - - -
-
-
-
-

DOTNETPROJECTS / MIGRATOR.NET

-

Database changes.
Part of your code.

-

- Write schema changes in C#. Version them with your application. - Run them with the database provider and ORM you choose. -

- -

- Open source · MPL-1.1 · Current source targets .NET 9 -

-
-
-
- - 001_CreateUsers.csUP / DOWN -
-
[Migration(1)]
-public class CreateUsers : Migration
-{
-    public override void Up()
-    {
-        Database.AddTable("Users",
-            new Column("Id", DbType.Int32,
-                ColumnProperty.NotNull),
-            new Column("Name", DbType.String, 255));
-        Database.AddPrimaryKey("PK_Users", "Users", "Id");
-    }
-
-    public override void Down()
-    {
-        Database.RemoveTable("Users");
-    }
-}
- -
-
-
-
-
- PROVIDER DIALECTSSQL ServerPostgreSQLSQLiteMySQL / MariaDBOracleSee all → -
-
-
-

SMALL API. EXPLICIT CONTROL.

-

- Your schema has a history.
Keep it in the repository. -

-
-
- 01 / AUTHOR -

C# without an ORM dependency

-

- Define tables, columns, indexes and constraints through a - transformation API. Use raw SQL when a change needs - database-specific behavior. -

-
-
- 02 / VERSION -

Move forward. Step back.

-

- Number your migrations, implement Up() and - Down(), and migrate to a chosen version. Applied - migrations are recorded in the database. -

-
-
- 03 / ORGANIZE -

Separate histories by scope

-

- Keep module version histories in one database using named scopes. - Select each module’s migration assembly or types when you create - its runner. -

-
-
-
-
-
-
-
-

QUICK START

-

From code to schema.

-
-

- A minimal SQLite example.
Use a .NET 9 console project for - the current source. -

-
-
-
- 1 -

Install the packages

-

- Add Migrator and an ADO.NET driver. This example passes an open - connection directly to the provider. -

- View package versions on NuGet ↗ -
-
-
- Terminal -
-
dotnet new console -n MigrationDemo -f net9.0
-cd MigrationDemo
-dotnet add package DotNetProjects.Migrator
-dotnet add package Microsoft.Data.Sqlite --version 9.0.7
-
-
-
-
- 2 -

Describe the change

-

- Add a public migration class. Each version must be unique within - the migration set loaded by a runner. -

-

- Down() is your explicit reverse operation; dropping - a table also removes its data. -

-
-
-
- CreateUsers.cs -
-
using System.Data;
-using DotNetProjects.Migrator.Framework;
-
-[Migration(1)]
-public class CreateUsers : Migration
-{
-    public override void Up()
-    {
-        Database.AddTable("Users",
-            new Column("Id", DbType.Int32,
-                ColumnProperty.NotNull),
-            new Column("Name", DbType.String, 255));
-        Database.AddPrimaryKey("PK_Users", "Users", "Id");
-    }
-
-    public override void Down()
-    {
-        Database.RemoveTable("Users");
-    }
-}
-
-
-
-
- 3 -

Run pending migrations

-

- Replace Program.cs with this code, then run - dotnet run. The runner discovers the migration in - your assembly and records it under the default scope. -

-

- Subsequent runs skip applied versions. Use - MigrateTo(version) to target an earlier or later - version. -

-
-
-
- Program.cs -
-
using DotNetProjects.Migrator;
-using DotNetProjects.Migrator.Providers;
-using Microsoft.Data.Sqlite;
-
-using var connection = new SqliteConnection("Data Source=app.db");
-connection.Open();
-
-using var provider = ProviderFactory.Create(
-    ProviderTypes.SQLite, connection, defaultSchema: null);
-
-var migrator = new Migrator(
-    provider, typeof(CreateUsers).Assembly, trace: false);
-
-if (migrator.LastAppliedMigrationVersion is long applied
-    && applied > migrator.AssemblyLastMigrationVersion)
-{
-    throw new InvalidOperationException(
-        "Database version is newer than this application.");
-}
-
-migrator.MigrateToLastVersion();
-
-
- -
-
-
-
-
-

DATABASE PROVIDERS

-

One API. Multiple dialects.

-
-

- Supply your ADO.NET driver.
Migrator supplies the schema - operations. -

-
-
-
-

Common database families

-
    -
  • SQL Server
  • -
  • PostgreSQL
  • -
  • SQLite
  • -
  • MySQL
  • -
  • MariaDB
  • -
  • Oracle
  • -
-
-
-

Additional dialects in source

-
    -
  • IBM Db2
  • -
  • IBM Informix
  • -
  • Firebird
  • -
  • Ingres
  • -
  • Sybase
  • -
-
-
-

- This is an implementation inventory, not a certification of every - server or driver version. Schema operations and transactional DDL vary - by provider. Check the - provider factory - and - provider tests - for your database. -

-
-
-
-
-
-

THE .NET MIGRATION LANDSCAPE

-

Choose by how you work.

-
-

- Feature comparison · Reviewed 22 September 2026
Read the sources and qualifications ↓ -

-
-

Source upgrade under review, not a NuGet release: - fluent operations, SQL-preview subset, runner options, native locks and source CLI. - Read the runner guide and limitations. - Follow the PR stack. -

-

- Migrator fits applications that want explicit C# migrations and - scoped history without coupling schema changes to an ORM. Other - tools offer different authoring and deployment workflows. -

-

- Read the detailed feature comparison (Markdown) →
- Explore SQLite emulation, preservation limits and framework - differences → -

-

- Scroll horizontally to compare all five frameworks on smaller - screens. -

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Built-in capabilities and documented workflows. “Custom” means - application code or configuration is needed. -
Capability - Migrator.NET DotNetProjects forkSource [1] - - FluentMigrator Sources [2] - - EF Core Sources [3] - - DbUp Sources [4] - - Evolve Sources [5] -
Authoring styleImperative C# + structured fluent APIHandwritten C#
Fluent DSL
C# generated from model changes; editableSQL scripts; C# scripts also supportedVersioned SQL files
ORM-independent workflowYesYesUses EF model and DbContextYesYes
- Generate migrations from model differences - No built-in generatorHand-authoredYes — model snapshotsHand-authoredHand-authored
Raw SQLExecuteNonQueryExecute.Sql / scriptsmigrationBuilder.SqlPrimary workflowPrimary workflow
Downgrade an applied version - Authored Down() or supported automatic reversal - - Down(); auto-reverse for supported expressions - Down(); target an earlier migrationForward fixes; custom undo workflowForward fixes; no Down command
History / module separation - Scope-filtered discovery + history - Custom version tables + migration filtering - Separate contexts / migrations + custom history tables - Separate journals + script filteringMetadata table/schema + script locations
TransactionsPer migration; none or verified whole-session modesPer migration by default; configurableMost migrations wrapped automaticallyOpt-in per script or whole run; none by defaultPer migration by default; whole-run option
Execution / deploymentLibrary + source CLI (unreleased)In-process runner + CLICLI, SQL scripts, bundles, runtime APILibrary; host in a console app or application.NET library, .NET tool, CLI
Database abstractionProvider dialects for schema operationsProvider-specific SQL generators - Relational providers; migrations may differ by provider - Database integrations; you write dialect-specific SQLDatabase integrations; you write dialect-specific SQL
Repeatable / recurring workOrdered maintenance + named profiles; no checksum repeatablesMaintenance migrations / profilesSeeding APIs (EF 9+); custom codeRunAlways scriptsRepeatable SQL reruns on checksum change
- -
-

- Rollback has two meanings. Reversing an already - applied migration uses authored reverse operations. Rolling back a - failed transaction depends on the database’s DDL support. Neither - restores data removed by a successful destructive migration. -

-

- Recurring work is not the same as change detection. - Evolve stores script checksums and validates changes; Migrator - records versions and scopes without built-in content checksum - validation. Maintenance hooks, seeding and RunAlways have - different execution rules. -

-
-
-
-

Keep migrations in C#

-

- Migrator: direct schema operations, scoped - history, and integration through your own host. - FluentMigrator: a fluent DSL with packaged - runners, tags and profiles. -

-
-
-

Let the model drive changes

-

- EF Core: a natural fit when an EF model defines - your schema and you want migration scaffolding, SQL generation - and deployment bundles. -

-
-
-

Keep SQL as the source

-

- DbUp: compose a script runner in .NET. - Evolve: convention-based versioned SQL, - checksum validation and repeatable scripts. -

-
-
-
- Sources & comparison methodology -

- Our column is based on the current repository source, which - targets net9.0. Other columns summarize official - documentation reviewed on 22 September 2026, rather than claiming - parity across every released package. Check your chosen release, - provider and database version. Suitability notes are our - interpretation of these documented capabilities. -

-
    -
  1. - DotNetProjects.Migrator: - target framework, - runner, - execution and transactions, - history and schema operations, - migration discovery. -
  2. -
  3. - FluentMigrator: - quick start and runners, - configuration and version tables, - auto-reversing migrations, - maintenance migrations, - profiles, - authoring and providers. -
  4. -
  5. - EF Core: - model snapshots, - authoring and transactions, - scripts, bundles and downgrade, - custom history tables, - multiple providers, - seeding. -
  6. -
  7. - DbUp: - execution, - transactions, - journaling, - script types, - forward-change philosophy, - SQL and C# script providers. -
  8. -
  9. - Evolve: - commands, checksums, repeatables and transactions, - configuration, - execution options. -
  10. -
-
- - -
-
-

CONTINUING MIGRATOR.NET

-

A familiar idea.
A maintained fork.

-

- DotNetProjects.Migrator continues the original Migrator.NET project, - bringing together fork contributions with work on SQLite schema - handling, provider independence and migration scopes. -

-
- -
- - - - - + + + + + + + + Migrator.NET — Database changes, in your code. + + + + + + + +
+
+
+
+

DOTNETPROJECTS / MIGRATOR.NET

+

Database changes.
Part of your code.

+

+ Write schema changes in C#. Version them with your application. + Run them with the database provider and ORM you choose. +

+ +

+ Open source · MPL-1.1 · Current source targets .NET 9 +

+
+
+
+ + 001_CreateUsers.csUP / DOWN +
+
[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32,
+                ColumnProperty.NotNull),
+            new Column("Name", DbType.String, 255));
+        Database.AddPrimaryKey("PK_Users", "Users", "Id");
+    }
+
+    public override void Down()
+    {
+        Database.RemoveTable("Users");
+    }
+}
+ +
+
+
+
+
+ PROVIDER DIALECTSSQL ServerPostgreSQLSQLiteMySQL / MariaDBOracleSee all → +
+
+
+

SMALL API. EXPLICIT CONTROL.

+

+ Your schema has a history.
Keep it in the repository. +

+
+
+ 01 / AUTHOR +

C# without an ORM dependency

+

+ Define tables, columns, indexes and constraints through a + transformation API. Use raw SQL when a change needs + database-specific behavior. +

+
+
+ 02 / VERSION +

Move forward. Step back.

+

+ Number your migrations, implement Up() and + Down(), and migrate to a chosen version. Applied + migrations are recorded in the database. +

+
+
+ 03 / ORGANIZE +

Separate histories by scope

+

+ Keep module version histories in one database using named scopes. + Select each module’s migration assembly or types when you create + its runner. +

+
+
+
+
+
+
+
+

QUICK START

+

From code to schema.

+
+

+ A minimal SQLite example.
Use a .NET 9 console project for + the current source. +

+
+
+
+ 1 +

Install the packages

+

+ Add Migrator and an ADO.NET driver. This example passes an open + connection directly to the provider. +

+ View package versions on NuGet ↗ +
+
+
+ Terminal +
+
dotnet new console -n MigrationDemo -f net9.0
+cd MigrationDemo
+dotnet add package DotNetProjects.Migrator
+dotnet add package Microsoft.Data.Sqlite --version 9.0.7
+
+
+
+
+ 2 +

Describe the change

+

+ Add a public migration class. Each version must be unique within + the migration set loaded by a runner. +

+

+ Down() is your explicit reverse operation; dropping + a table also removes its data. +

+
+
+
+ CreateUsers.cs +
+
using System.Data;
+using DotNetProjects.Migrator.Framework;
+
+[Migration(1)]
+public class CreateUsers : Migration
+{
+    public override void Up()
+    {
+        Database.AddTable("Users",
+            new Column("Id", DbType.Int32,
+                ColumnProperty.NotNull),
+            new Column("Name", DbType.String, 255));
+        Database.AddPrimaryKey("PK_Users", "Users", "Id");
+    }
+
+    public override void Down()
+    {
+        Database.RemoveTable("Users");
+    }
+}
+
+
+
+
+ 3 +

Run pending migrations

+

+ Replace Program.cs with this code, then run + dotnet run. The runner discovers the migration in + your assembly and records it under the default scope. +

+

+ Subsequent runs skip applied versions. Use + MigrateTo(version) to target an earlier or later + version. +

+
+
+
+ Program.cs +
+
using DotNetProjects.Migrator;
+using DotNetProjects.Migrator.Providers;
+using Microsoft.Data.Sqlite;
+
+using var connection = new SqliteConnection("Data Source=app.db");
+connection.Open();
+
+using var provider = ProviderFactory.Create(
+    ProviderTypes.SQLite, connection, defaultSchema: null);
+
+var migrator = new Migrator(
+    provider, typeof(CreateUsers).Assembly, trace: false);
+
+if (migrator.LastAppliedMigrationVersion is long applied
+    && applied > migrator.AssemblyLastMigrationVersion)
+{
+    throw new InvalidOperationException(
+        "Database version is newer than this application.");
+}
+
+migrator.MigrateToLastVersion();
+
+
+ +
+
+
+
+
+

DATABASE PROVIDERS

+

One API. Multiple dialects.

+
+

+ Supply your ADO.NET driver.
Migrator supplies the schema + operations. +

+
+
+
+

Common database families

+
    +
  • SQL Server
  • +
  • PostgreSQL
  • +
  • SQLite
  • +
  • MySQL
  • +
  • MariaDB
  • +
  • Oracle
  • +
+
+
+

Additional dialects in source

+
    +
  • IBM Db2
  • +
  • IBM Informix
  • +
  • Firebird
  • +
  • Ingres
  • +
  • Sybase
  • +
+
+
+

+ This is an implementation inventory, not a certification of every + server or driver version. Schema operations and transactional DDL vary + by provider. Check the + provider factory + and + provider tests + for your database. +

+
+
+
+
+
+

THE .NET MIGRATION LANDSCAPE

+

Choose by how you work.

+
+

+ Feature comparison · Reviewed 22 September 2026
Read the sources and qualifications ↓ +

+
+

Source upgrade under review, not a NuGet release: + fluent operations, SQL-preview subset, runner options, native locks and source CLI. + Read the runner guide and limitations. + Follow the PR stack. +

+

+ Migrator fits applications that want explicit C# migrations and + scoped history without coupling schema changes to an ORM. Other + tools offer different authoring and deployment workflows. +

+

+ Read the detailed feature comparison (Markdown) →
+ Explore SQLite emulation, preservation limits and framework + differences → +

+

+ Scroll horizontally to compare all five frameworks on smaller + screens. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Built-in capabilities and documented workflows. “Custom” means + application code or configuration is needed. +
Capability + Migrator.NET DotNetProjects forkSource [1] + + FluentMigrator Sources [2] + + EF Core Sources [3] + + DbUp Sources [4] + + Evolve Sources [5] +
Authoring styleImperative C# + structured fluent APIHandwritten C#
Fluent DSL
C# generated from model changes; editableSQL scripts; C# scripts also supportedVersioned SQL files
ORM-independent workflowYesYesUses EF model and DbContextYesYes
+ Generate migrations from model differences + No built-in generatorHand-authoredYes — model snapshotsHand-authoredHand-authored
Raw SQLExecuteNonQueryExecute.Sql / scriptsmigrationBuilder.SqlPrimary workflowPrimary workflow
Downgrade an applied version + Authored Down() or supported automatic reversal + + Down(); auto-reverse for supported expressions + Down(); target an earlier migrationForward fixes; custom undo workflowForward fixes; no Down command
History / module separation + Scope-filtered discovery + history + Custom version tables + migration filtering + Separate contexts / migrations + custom history tables + Separate journals + script filteringMetadata table/schema + script locations
TransactionsPer migration; none or verified whole-session modesPer migration by default; configurableMost migrations wrapped automaticallyOpt-in per script or whole run; none by defaultPer migration by default; whole-run option
Execution / deploymentLibrary + source CLI (unreleased)In-process runner + CLICLI, SQL scripts, bundles, runtime APILibrary; host in a console app or application.NET library, .NET tool, CLI
Database abstractionProvider dialects for schema operationsProvider-specific SQL generators + Relational providers; migrations may differ by provider + Database integrations; you write dialect-specific SQLDatabase integrations; you write dialect-specific SQL
Repeatable / recurring workOrdered maintenance + named profiles; no checksum repeatablesMaintenance migrations / profilesSeeding APIs (EF 9+); custom codeRunAlways scriptsRepeatable SQL reruns on checksum change
+
+
+

+ Rollback has two meanings. Reversing an already + applied migration uses authored reverse operations. Rolling back a + failed transaction depends on the database’s DDL support. Neither + restores data removed by a successful destructive migration. +

+

+ Recurring work is not the same as change detection. + Evolve stores script checksums and validates changes; Migrator + records versions and scopes without built-in content checksum + validation. Maintenance hooks, seeding and RunAlways have + different execution rules. +

+
+
+
+

Keep migrations in C#

+

+ Migrator: direct schema operations, scoped + history, and integration through your own host. + FluentMigrator: a fluent DSL with packaged + runners, tags and profiles. +

+
+
+

Let the model drive changes

+

+ EF Core: a natural fit when an EF model defines + your schema and you want migration scaffolding, SQL generation + and deployment bundles. +

+
+
+

Keep SQL as the source

+

+ DbUp: compose a script runner in .NET. + Evolve: convention-based versioned SQL, + checksum validation and repeatable scripts. +

+
+
+
+ Sources & comparison methodology +

+ Our column is based on the current repository source, which + targets net9.0. Other columns summarize official + documentation reviewed on 22 September 2026, rather than claiming + parity across every released package. Check your chosen release, + provider and database version. Suitability notes are our + interpretation of these documented capabilities. +

+
    +
  1. + DotNetProjects.Migrator: + target framework, + runner, + execution and transactions, + history and schema operations, + migration discovery. +
  2. +
  3. + FluentMigrator: + quick start and runners, + configuration and version tables, + auto-reversing migrations, + maintenance migrations, + profiles, + authoring and providers. +
  4. +
  5. + EF Core: + model snapshots, + authoring and transactions, + scripts, bundles and downgrade, + custom history tables, + multiple providers, + seeding. +
  6. +
  7. + DbUp: + execution, + transactions, + journaling, + script types, + forward-change philosophy, + SQL and C# script providers. +
  8. +
  9. + Evolve: + commands, checksums, repeatables and transactions, + configuration, + execution options. +
  10. +
+
+
+
+
+
+

CONTINUING MIGRATOR.NET

+

A familiar idea.
A maintained fork.

+

+ DotNetProjects.Migrator continues the original Migrator.NET project, + bringing together fork contributions with work on SQLite schema + handling, provider independence and migration scopes. +

+
+ +
+
+ + + + diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index e9748021..1007de92 100644 --- a/docs/migration-framework-comparison.md +++ b/docs/migration-framework-comparison.md @@ -1,410 +1,410 @@ -# .NET database migration frameworks: detailed feature comparison - -**Reviewed: 22 September 2026.** This is a capability comparison, not a benchmark or an overall ranking. - -The main matrices cover **DotNetProjects.Migrator, FluentMigrator, EF Core migrations, DbUp and Evolve**—all five frameworks on the homepage. Additional sections cover **EF6, grate and RoundhousE**, with a short boundary comparison for **Flyway and Liquibase**. This is a defined shortlist, not a claim to catalogue every migration package ever published. - -Migrator findings are pinned to upgrade-stack commit [`874cb88`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. - -[Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Project README](../README.md) · [SQLite emulation comparison](#sqlite-emulation-comparison) · [Source index](#source-index) - -## Contents - -- [How to read the matrices](#how-to-read-the-matrices) -- [Authoring and application integration](#authoring-and-application-integration) -- [Schema and data operations](#schema-and-data-operations) -- [History, ordering and repeatability](#history-ordering-and-repeatability) -- [Transactions, rollback and coordination](#transactions-rollback-and-coordination) -- [Deployment, inspection and configuration](#deployment-inspection-and-configuration) -- [Database coverage and portability](#database-coverage-and-portability) -- [SQLite emulation comparison](#sqlite-emulation-comparison) -- [EF6, grate and RoundhousE](#ef6-grate-and-roundhouse) -- [Flyway and Liquibase in a .NET deployment](#flyway-and-liquibase-in-a-net-deployment) -- [Choosing a framework and identifying Migrator gaps](#choosing-a-framework-and-identifying-migrator-gaps) -- [Validation and maintenance](#validation-and-maintenance) -- [Source index](#source-index) - -## How to read the matrices - -| Term | Meaning | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Built-in / named API | The reviewed tool provides this operation or workflow. Database restrictions still apply. | -| Configure | Available through documented runner settings, composition or extension points. | -| Custom | You supply application code, SQL or deployment orchestration. Not automatic framework behavior. | -| No built-in | No implementation in the inspected Migrator source, or no equivalent in the reviewed documented workflow. It does not rule out third-party extensions. | -| Provider-dependent | Availability or semantics depend on the database integration and release. | -| Not verified | Evidence is insufficient for a positive or negative compatibility claim. | - -A SQL runner can execute a hand-authored table rebuild; that does **not** mean it automatically emulates `AlterColumn`. Likewise, recording applied migrations is not schema-drift detection, a transaction is not a deployment mutex, and a version downgrade is not a data restore. - -## Authoring and application integration - -Evidence: [Migrator runner][m-runner], [loader][m-loader], [migration contract][m-migration]; [FluentMigrator quick start][f-start] and [SQL execution][f-sql]; [EF Core overview][ef-overview] and [managing migrations][ef-managing]; [DbUp usage][d-usage] and [script providers][d-providers]; [Evolve concepts][e-concepts] and [configuration][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| ---------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------- | ------------------------------------- | ------------------------------------------ | -| Primary authoring artifact | Public C# migration class | C# migration class with fluent expressions | Generated, editable C# migration + model snapshot | SQL file or C# `IScript` | Versioned SQL file | -| Requires an ORM model | No | No | Yes, for normal scaffolding | No | No | -| Generates changes from model differences | No built-in | No built-in model differ in core workflow | Yes | No; author scripts | No; author scripts | -| Migration without a model change | Yes | Yes | Empty migration, then custom operations | Yes | Yes | -| Schema DSL / transformation API | Imperative API and structured `MigrationBuilder`; provider limits apply | Fluent create/alter/delete expressions | `MigrationBuilder` operations | No schema DSL; SQL / commands | No schema DSL; SQL | -| Custom C# logic | `Up` / `Down`; open provider | Migration code / connection operations | SQL/custom operations for database work | `IScript` and command factory | Surrounding host logic; migrations are SQL | -| Raw SQL | Command, query and scalar APIs | Inline, file and embedded SQL | `migrationBuilder.Sql` | Primary workflow | Primary workflow | -| Migration discovery | Assembly scan or explicit `Type[]` | Assembly scanning / filters | Context's migration assembly | Configurable script providers | Locations or embedded resources | -| Constructor dependency injection | Optional Microsoft DI/options package; custom activator supported | Runner/DI integration | Context services; migration customization is separate | Custom script provider/host if needed | No C# migration constructors | -| Embedded execution | Yes | Yes | Yes | Yes | Library mode | -| Dedicated execution host | Library or source-built packaged .NET tool (unreleased) | Library or packaged runner | Tooling, bundles or custom host | Write your own | CLI, .NET tool or library | - -EF Core's model snapshot comparison is not a live-database schema comparison. DbUp's C# support is more than static SQL file loading, but it does not supply a cross-database schema-operation layer. - -## Schema and data operations - -This table separates having an authoring API from that API working identically on every engine. SQLite is broken out below. Evidence: [Migrator interface][m-api] and [provider factory][m-factory]; [FluentMigrator operations][f-start]; [EF Core migration operations][ef-managing]; [DbUp script execution][d-usage]; [Evolve SQL model][e-concepts]. - -| Operation family | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| --------------------------------------- | ---------------------------------------- | ----------------------------------- | ------------------------------------------ | ----------------------------- | ------------------------------------- | -| Create / drop table | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | -| Rename table | Schema API | Fluent API | Migration operation | Author SQL | Author SQL | -| Add / drop / rename column | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | -| Change type / nullability / default | `ChangeColumn` and default API | Alter expressions | `AlterColumn` | Author SQL | Author SQL | -| Primary / composite keys | API; provider-dependent | Fluent expressions | Migration operations | Author SQL | Author SQL | -| Foreign keys / delete behavior | API; mapped constraint types | Fluent expressions | Migration operations | Author SQL | Author SQL | -| Unique constraints | API | Fluent expressions | Migration operations | Author SQL | Author SQL | -| Check constraints | API using SQL predicate | Provider/custom SQL as applicable | Migration operations | Author SQL | Author SQL | -| Indexes | API and index model | Fluent expressions | Operations / provider annotations | Author SQL | Author SQL | -| Filtered / included / clustered indexes | Provider-specific subsets | Provider-specific options | Provider-specific support | Engine-specific SQL | Engine-specific SQL | -| Views | `AddView` and SQL | Usually SQL | Usually SQL migrations | Author SQL | SQL; repeatables useful | -| Stored procedures / triggers | Raw SQL | SQL / connection operations | SQL / custom operations | Author SQL | Author SQL | -| Fixed-data insert / update / delete | Data API | Fluent data expressions | `InsertData` / `UpdateData` / `DeleteData` | SQL or C# | SQL | -| Transform existing data | SQL, provider reads/writes, copy helpers | SQL / connection operations | SQL / custom operations | SQL or C# | SQL | -| Live table / column existence | Existence and metadata APIs | Schema query API | SQL/custom code | SQL or C# | SQL | -| Full schema-drift report | No built-in | Not established by version tracking | Snapshot comparison alone is insufficient | Journal alone is insufficient | Checksums concern scripts, not schema | - -## History, ordering and repeatability - -Evidence: [Migrator loader][m-loader], [execution][m-execution] and [history storage][m-provider]; [FluentMigrator configuration][f-config], [maintenance][f-maintenance] and [profiles][f-profiles]; [EF Core overview][ef-overview], [history][ef-history] and [seeding][ef-seeding]; [DbUp journaling][d-journal], [script types][d-types] and [usage][d-usage]; [Evolve concepts][e-concepts] and [options][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| -------------------------- | ------------------------------------------------------- | -------------------------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------------- | -| Applied-change identity | Numeric version within scope | Migration version | Migration ID | Script name | Script metadata | -| Default history | `SchemaInfo` | Version table | `__EFMigrationsHistory` | E.g. `SchemaVersions` | `changelog` | -| History customization | Table name; scope column | Version-table metadata | Table/schema; custom services | Custom journal / table | Metadata table/schema | -| Independent modules | Scope + selected migrations | Separate history + filters | Contexts/assemblies + separate history | Filters + separate journals | Locations + separate metadata | -| Environment selection | Tags with explicit Any/All matching; scopes and named profiles | Tags / profiles / configuration | Context/deployment configuration | Filters / host | Locations / placeholders / host | -| Skip applied work | Version history | Version history | Migration history | Journal | Metadata | -| Applied-source checksum | No built-in | Not a core version-table guarantee | No script checksum journal | Standard journal tracks names; custom validation | Script checksums | -| Late lower-numbered change | Revisits missing versions up to target | Check runner policy | Do not assume IDs make diverging branches safe | Unrecorded scripts eligible; ordering matters | `OutOfOrder` | -| Repeat on content change | Custom | Not equivalent to maintenance/profiles | Not equivalent to seeding | Custom checksum-aware runner | Repeatable SQL | -| Always-run work | Ordered before/after-run and before/after-migration stages; selected profiles | Maintenance / selected profiles | Seeding APIs, EF 9+ | `RunAlways` / `NullJournal` | Not identical to RunAlways | -| Existing-schema baseline | Custom verified history initialization | Custom baseline/runner strategy | Existing-schema workflow | `MarkAsExecuted` | `StartVersion` / skip options | -| Repair checksums | Not applicable | Not established by version history | Not applicable | Custom journal concern | `repair` | - -**Migrator scope detail:** unscoped migrations inherit the runner scope. Explicitly scoped migrations are selected only for that scope; duplicate validation and history access use the same effective scope. Custom legacy providers without `IMigrationHistory` retain their prior behavior. History isolation is not table isolation. [Loader][m-loader], [execution][m-execution], [provider][m-provider]. - -## Transactions, rollback and coordination - -Evidence: [Migrator execution][m-execution] and [runner][m-runner]; [FluentMigrator configuration][f-config] and [auto-reverse][f-reverse]; [EF Core management][ef-managing], [deployment][ef-applying] and [SQLite limitations][ef-sqlite]; [DbUp transactions][d-transactions] and [philosophy][d-philosophy]; [Evolve concepts][e-concepts] and [options][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| ------------------------------ | -------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------ | -| Default transaction unit | Per migration | Per migration; configurable | Version-sensitive: EF 9 grouped pending migrations, reverted in EF 10 | None | Per migration | -| Whole-run transaction | `WholeSession` for verified SQLite, PostgreSQL and SQL Server dialects | Configure/orchestrate; check runner | Depends on version/operations | `WithTransaction()` | `CommitAll` | -| Per-change transaction opt-out | Run-level `None`; no per-migration transaction attribute | Transaction behavior | Raw SQL suppression | Choose strategy / separate runs | Script opt-out | -| Failed DDL rollback | Engine-dependent | Engine-dependent | Engine-dependent | When enabled and supported | Engine-dependent | -| Reverse committed migration | Authored `Down()` | `Down()` | Generated/editable `Down()` | Custom undo / forward fix | Forward fix; no Down command | -| Generate reverse operations | Supported create/rename operations; explicit reverse required for destructive/data/SQL operations | Supported auto-reverse expressions | Scaffolding; review output | No schema reverse generator | No | -| Target earlier version | `MigrateTo` | Down/rollback APIs | Earlier target / reverse script | Custom | Target limits forward work, not undo | -| Restore deleted data | Backup / reconstruction | Same | Same | Same | Same | -| Cross-process coordination | Opt-in native session locks for SQL Server, PostgreSQL and MySQL/MariaDB; custom abstraction | Serialize deployment / application-lock pattern | Migration locking, EF 9+; execution-path dependent | Host/provider concern; journal is not a lock | Cluster setting; provider-dependent | -| Post-commit hooks | `AfterUp` / `AfterDown` | Maintenance stages | Host/seeding lifecycle; not direct equivalent | Host / ordered scripts | Host / ordered scripts | - -A scope, checksum, history primary key or ordinary database write lock does not prove that two deployments can safely run the entire sequence concurrently. Evolve's cluster setting must be checked for the selected provider; it is not a blanket SQLite session-lock guarantee. - -## Deployment, inspection and configuration - -Evidence: [Migrator runner][m-runner] and [execution][m-execution]; [FluentMigrator runners][f-start] and [configuration][f-config]; [EF Core deployment][ef-applying]; [DbUp usage][d-usage], [variables][d-variables] and [logging][d-logging]; [Evolve execution][e-start] and [options][e-options]. - -| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | -| ------------------------------------ | ------------------------------------------------- | ------------------------------------------ | --------------------------------- | -------------------------------- | ------------------------------------------------ | -| Packaged CLI | Source project `DotNetProjects.Migrator.Tool`; not published by this upgrade | Yes | `dotnet ef` | Core library; custom host | Yes | -| Dedicated migration bundle generator | No; publish host | Package runner/migrations | Yes | Publish host | CLI distribution, not EF-style bundle generation | -| Review SQL without applying | Connected/offline structured subset; unsupported operations fail explicitly | Preview/output | Scripts | Authored SQL / pending scripts | Authored SQL | -| Dry-run qualification | `DryRun` plans versions without migration bodies, callbacks, transactions or history creation | Processor preview; user code needs care | Not a full side-effect simulation | Pending list / custom simulation | `RollbackAll` actually executes | -| Idempotent deployment SQL | Custom | Preview is not idempotent history guarding | Provider-dependent; not SQLite | Author SQL / use journal | Author SQL / use metadata | -| Status | Versions / loaded types | Runner/tool info | CLI / history APIs | Pending/executed APIs | `info` | -| Command timeout | Provider setting | Processor setting | Database/provider setting | Runner/provider setting | `CommandTimeout` | -| Logging | Legacy logger plus optional Microsoft logging adapter (SQL/exception details omitted) | Logging integration | EF logging | `IUpgradeLog` / integrations | Host/CLI | -| SQL substitution | Custom | Script tokens | Custom logic | `$variable$` | `${placeholder}` | -| Deployment identity | Host connection | Runner connection | Migration connection | Host connection | Tool connection | - -**Migrator dry run is not an offline SQL preview.** Execution starts provider work while `Up()`/`Down()` are skipped. It cannot show SQL from those skipped bodies and should not be described as side-effect-free database validation. [Execution source][m-execution]. - -## Database coverage and portability - -| Framework | How support is supplied | What it does not guarantee | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -| Migrator | Source dialects + separate ADO.NET drivers. Live CI covers SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix and Sybase; Ingres is another source dialect. [CI guide][m-live]. | Every server/driver release, operation or arbitrary SQL construct. | -| FluentMigrator | Provider generators/processors. [Configuration][f-config]. | The same expression working on every engine. | -| EF Core | Relational provider packages. [Multiple providers][ef-providers]. | One provider's generated migrations working unchanged elsewhere. | -| DbUp | Database integrations. [Provider list][d-databases]. | SQL dialect translation. | -| Evolve | Database integrations. [Requirements][e-requirements]. | SQL translation or identical transactions. | - -A migration can compile yet require a table copy, lose an unsupported schema detail or fail on existing data. Compare the exact operation and data shape, not just database names. - -## SQLite emulation comparison - -### What emulation means - -SQLite has native table rename, column rename, add-column and (on sufficiently recent engines, subject to restrictions) drop-column operations. SQLite 3.53.0 added native `ALTER COLUMN … SET/DROP NOT NULL`; it still does not provide general type/default alteration or `ALTER TABLE ADD/DROP CONSTRAINT`. More complex changes require a replacement table, copying rows and rebuilding dependent objects. Native capabilities evolve independently of the .NET driver package. [SQLite ALTER TABLE reference][sqlite-alter]. - -Migrator reads the **live schema** into `SQLiteTableInfo`, modifies that representation and calls `RecreateTable`. It creates `Temp`, copies mapped columns with `INSERT … SELECT`, drops the original, renames the replacement and recreates represented indexes. This works without an ORM model, but depends on what its schema reader can represent. [Implementation][m-sqlite], [schema model][m-sqlite-model]. - -### Automatic operation matrix - -**R** = built-in rebuild; **N** = native SQL path, subject to engine restrictions; **U** = unique-index substitution; **Manual** = author the change/rebuild yourself; **Manual** also covers a generated statement that the engine does not support. Rows describe **changes to an existing table**, not constraints declared when creating it. - -The combined SQL-runner column applies **individually to DbUp and Evolve**: both execute supplied SQL rather than diffing/rebuilding the schema. grate and RoundhousE follow the same distinction. Manual does not mean the engine cannot perform the operation. - -Evidence: [EF Core SQLite operation table][ef-sqlite], [FluentMigrator SQLite generator][f-sqlite-generator], [inherited SQL templates][f-generic-generator] and [processor][f-sqlite-processor], [DbUp scripts][d-usage], [Evolve concepts][e-concepts]. Migrator cells are supported by the source/test inventory below. - -| Existing-table operation | Migrator | FluentMigrator | EF Core | DbUp / Evolve | -| ---------------------------- | --------------------------------- | ------------------------------- | ----------- | ---------------------------------------------- | -| Add ordinary column | R | N | N | Manual SQL | -| Remove column | R | N; engine restrictions | R | Manual SQL/rebuild | -| Rename column | N on SQLite 3.26+; R fallback | N; engine restrictions | N | Manual SQL/rebuild | -| Change declared type | R | Manual | R | Manual rebuild | -| Change nullability | R | Manual | R | Manual SQL on 3.53+ / rebuild on older engines | -| Change default | R via full `Column` | Manual | R via alter | Manual rebuild | -| Remove default | R via dedicated API; caveat below | Manual | R via alter | Manual rebuild | -| Add primary key | R | Manual | R | Manual rebuild | -| Remove primary key | R | Manual | R | Manual rebuild | -| Add foreign key | R | Manual | R | Manual rebuild | -| Remove foreign key | R | Manual | R | Manual rebuild | -| Add unique constraint | R | U | R | Manual rebuild/index | -| Remove unique constraint | R | U for tool-created unique index | R | Manual rebuild/index | -| Add check constraint | R | Manual | R | Manual rebuild | -| Remove check constraint | R | Manual | R | Manual rebuild | -| Create / drop ordinary index | N | N | N | Manual SQL | -| Rename table | N | N | N | Manual SQL | - -The table describes framework paths, not everything the newest SQLite engine can do. Migrator still rebuilds for nullability changes; FluentMigrator still rejects its general alter-column expression even when a newer engine can execute a hand-authored NOT NULL alteration. - -EF Core rebuilds rely on model-represented artifacts; the docs identify failures for artifacts outside that model. EF 9+ uses a SQLite lock table with abandoned-lock recovery considerations. These are separate from rebuild support. [SQLite limitations][ef-sqlite]. - -FluentMigrator supports inline FKs during table creation. Its reviewed generator directs callers to manual reconstruction for later FK changes; `LOOSE` mode skips unsupported expressions rather than emulating them. Unique-index substitution does not imply that an existing table-level UNIQUE constraint can be dropped as an index. [Generator][f-sqlite-generator]. - -### Migrator's emulated operations, precisely - -Methods refer to the pinned [SQLite provider][m-sqlite]. Tests illustrate evidence, not exhaustive coverage of every data/schema combination. - -| API / operation | Implementation behavior | Qualification / evidence | -| ------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `AddColumn` | Adds a column and mapping without an old source column; rebuilds. | Existing rows receive SQLite default/NULL behavior; incompatible NOT NULL requirements can fail. [Tests][t-add-column]. | -| `ChangeColumn` | Replaces the entire matching `Column` definition; rebuilds. | Specify properties to retain. Type affinity during copying is not arbitrary data conversion. [Tests][t-change-column]. | -| `RemoveColumnDefaultValue` | Clears parsed default; rebuilds. Generic default-removal regression is enabled and passes. | Dedicated and generic default-removal regressions run; provider CI is required for changes. [Tests][t-sqlite-general]. | -| `RemoveColumn` | Removes column/mapping and matching single-column indexes/uniques/FKs; rebuilds affected tables. | Rejects detected CHECK references and composite dependencies until adjusted. Can remove inbound single-column FKs from other tables. [Tests][t-remove-column]. | -| `RenameColumn` | Native on SQLite 3.26+; reconstruction fallback for older engines. | Native rename delegates dependency rewriting to SQLite; reconstruction is not an arbitrary SQL-expression rewriter. [Tests][t-rename-column]. | -| `AddPrimaryKey` | Sets membership, orders selected columns, rebuilds. | Composite keys supported; `PrimaryKeyExists` checks for any PK rather than matching its name. [Tests][t-pk]. | -| `RemovePrimaryKey` | Clears PK/PK-identity flags; rebuilds. | Changes identity-related semantics; review referencing tables. [Source][m-sqlite]. | -| `AddForeignKey` / `RemoveForeignKey` | Adds/removes represented FK; rebuilds child table. | Validate existing rows and enforcement. [FK tests][t-fk], [integrity tests][t-integrity]. | -| `AddUniqueConstraint` | Adds named unique definition; rebuilds. | Duplicate data can reject the copy. [Metadata tests][t-uniques]. | -| `AddCheckConstraint` | Adds named CHECK SQL; rebuilds. | Predicate must accept existing rows and be understood by the reader. [Tests][t-check]. | -| `RemoveConstraint` | Removes matching unique and check definitions; rebuilds. | Does not remove FKs/PKs; use dedicated APIs. [Source][m-sqlite]. | -| `RemoveAllConstraints` | Clears PK, unique, FK and CHECK definitions before rebuilding. | Constraint removal can fail when dependent schemas/data require a coordinated migration. [Tests][t-remove-constraints], [source][m-sqlite]. | -| `RemoveAllIndexes` | Clears indexes **and unique constraints**; rebuilds. | Broader than dropping non-unique indexes. [Source][m-sqlite]. | -| `RecreateTable` | Public low-level schema/mapping reconstruction. | Requires a consistent supported representation. [Composite-key round-trip test][t-recreate]. | -| `TruncateTable` | Emits `DELETE FROM`. | Not native TRUNCATE and not an identity-sequence reset. [Source][m-sqlite]. | - -### What survives reconstruction—and what is not guaranteed - -| Schema/data detail | Migrator at the pinned revision | Implication | -| ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| Mapped rows | Named-column `INSERT … SELECT`. | New constraints/types must accept the data. | -| Names, parsed types, nullability, defaults | Included in column model. | Not a lossless representation of arbitrary CREATE SQL. | -| Composite PKs | Represented; dedicated rebuild test. | Check membership/order when replacing definitions. | -| FKs and delete actions | Read from schema/PRAGMA; emitted into replacement DDL. | Not a promise about every clause, e.g. arbitrary deferrability. | -| Unique / CHECK definitions | Included in `SQLiteTableInfo`. | Reader restrictions apply; rename does not rewrite arbitrary CHECK expressions. | -| Indexes / represented filters | Recreated after replacement. | Complex predicates, expressions, collations and sort details require separate verification. | -| Triggers | Collected and replayed for supported rebuilds without renames; unsafe rename fallback rejected. | Trigger SQL is replayed only where the rebuild does not require rewriting its identifiers. | -| Views / dependent SQL | No general dependency-SQL rewrite. | Validate/recreate dependencies after renames/drops. | -| `WITHOUT ROWID`, `STRICT`, generated columns | Unsupported reconstruction is rejected before dropping the original. | No preservation claim for unsupported external table properties. | -| Hidden `rowid` / AUTOINCREMENT high-water mark | Only mapped columns copied; no explicit sequence-state restoration. | Historical rowid/sequence metadata may change. | -| Type / length enforcement | Changes declarations, not SQLite typing rules. | Declared size is not SQL Server-like length enforcement. | -| FK enforcement state | Runner and owned rebuild transactions restore the prior setting after success/failure. | Caller-owned active SQLite transactions require FK settings to be configured before beginning the transaction. | -| Whole-database FK validation | Runner and owned rebuild transactions validate integrity before commit. | Enabling enforcement alone does not validate existing rows. | - -Evidence: [SQLite provider][m-sqlite], [schema model][m-sqlite-model], [execution][m-execution], [SQLite reconstruction procedure][sqlite-alter]. Native drop-column selection, sequence high-water preservation and arbitrary dependency rewriting remain gaps. - -### How the other frameworks compare on preservation - -| Framework | Replacement schema source | Responsibility for unsupported dependencies | -| ------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -| Migrator | Live reader + `SQLiteTableInfo`. | Author handles objects outside the representation. | -| EF Core | Model/migration metadata. | Author handles artifacts outside automatic model-based rebuilding. [Docs][ef-sqlite]. | -| FluentMigrator | No general rebuild engine found in inspected SQLite components. | Author writes reconstruction for unsupported alterations. [Generator][f-sqlite-generator], [processor][f-sqlite-processor]. | -| DbUp | Project SQL / C#. | Script author. [Usage][d-usage]. | -| Evolve | Project SQL. | Script author. [Concepts][e-concepts]. | -| grate / RoundhousE | Project SQL. | Script author. Database integration is not emulation. [grate][g-home], [RoundhousE][r-home]. | -| EF6 | Selected provider's migration generator. | Provider-specific; EF Core rebuild support must not be attributed to EF6. No specific EF6 SQLite emulation verified here. | - -**Practical conclusion:** Migrator's differentiator is live-schema-based SQLite reconstruction without an ORM model. It is not unique in automatic SQLite rebuilding—EF Core also does this—and is not a lossless rewriter of every SQLite schema feature. - -## EF6, grate and RoundhousE - -Evidence: [EF6 migrations][ef6-main], [automatic migrations][ef6-auto], [history][ef6-history], [CLI][ef6-cli]; [grate home][g-home], [configuration][g-config], [script types][g-types], [anytime][g-anytime], [everytime][g-everytime], [one-time][g-onetime]; [RoundhousE][r-home] and [grate migration guide][g-migrate]. - -| Capability | EF6 Code First | grate | RoundhousE | -| ------------------------------------------- | --------------------------------------------- | --------------------------------- | ----------------------------------------------- | -| Authoring | C# from EF6 model | Lifecycle SQL folders | Lifecycle SQL folders | -| ORM dependency | EF6 model/context | None | None | -| Model-difference generation | Yes | No | No | -| Automatic migrations without explicit files | Optional EF6 feature | No | No | -| Reverse version | `Down`, target migration | Forward/custom recovery | Forward/custom recovery | -| Change once | Versioned migration | One-time scripts | One-time scripts | -| Run after content change | Not a SQL repeatable mechanism | Anytime scripts | Anytime workflow | -| Every deployment | Seed/custom lifecycle | Everytime scripts | Everytime workflow | -| Detect script edits | Not a script checksum journal | One-time hash checking | Changed-script policies | -| Existing-schema baseline | Existing-schema workflow | `--baseline` | Verify release's workflow | -| Transactions | EF/provider execution | Opt-in `--transaction` | Transaction flags / outside-transaction scripts | -| Environment filtering | Host/configuration | Filename conventions | Environment scripts | -| SQL token replacement | Custom | User tokens | Tokens | -| History separation | Context history / customization | Migration schema/configuration | Repository/schema conventions | -| Preview / inspection | Script generation | `--dryrun`, logs | Check release's dry-run/log tooling | -| Execution | PMC/runtime; `ef6.exe` replaces `migrate.exe` | CLI; self-contained distributions | CLI / .NET tooling | -| Automatic SQLite emulation | Provider-specific; not verified | None in documented workflow | None in documented workflow | - -RoundhousE maintainers point to grate as a successor. The migration guide documents differences; do not assume parity for every flag, history configuration or folder. This is a compatibility consideration, not a claim of identical release/support status. - -## Flyway and Liquibase in a .NET deployment - -These can migrate databases used by .NET applications, but do not replace Migrator's in-process C# transformation API directly. This narrower comparison avoids folding edition-dependent features into the main matrices. - -| Concern | Flyway | Liquibase | -| ------------------------- | ----------------------------------------------------------------- | --------------------------------------------------- | -| Artifacts | Versioned / repeatable migrations | Changelog changesets, including formatted SQL | -| Recovery | Explicit undo migrations where the selected edition supports Undo | Change-type-dependent / authored rollback | -| Selection and assumptions | Tool configuration; check command/edition | Contexts/preconditions; format/version restrictions | -| Automatic SQLite rebuild | Not established here; supplied SQL is not emulation | Not established here; verify change type/extension | -| .NET integration | Separate deployment tool | Separate deployment tool | - -Sources: [Flyway Undo][flyway-undo], [baseline migrations][flyway-baseline], [Liquibase rollback][liquibase-rollback], [preconditions][liquibase-preconditions]. This document does not claim that every command is available in a free edition. - -## Choosing a framework and identifying Migrator gaps - -These interpretations are grounded in the preceding evidence, rather than universal recommendations. - -| Requirement | Candidate / tradeoff | -| ---------------------------------------------- | ----------------------------------------------------------------------- | -| No ORM model, frequent SQLite alterations | Evaluate Migrator's live-schema reconstruction and preservation limits. | -| EF model defines schema | EF Core supplies scaffolding, rebuilds and deployment artifacts. | -| Handwritten C# / packaged runners / fluent DSL | FluentMigrator; manual work for unsupported SQLite alterations. | -| SQL-first runner composed in .NET | DbUp's script providers, journal and transaction strategies. | -| SQL checksums / change-triggered repeatables | Evolve's built-in conventions. | -| Existing RoundhousE folders | Evaluate grate's migration guide and history compatibility. | -| Existing EF6 application | Assess EF6/provider behavior separately from EF Core. | -| Multi-language database-owned deployment | Evaluate Flyway/Liquibase and required editions. | - -Potential Migrator improvements, **not implemented-feature claims**: - -1. Broader structured SQL-preview coverage, provider-specific batch scripts and CLI deployment validation. The source CLI and preview subset already exist. -2. Validation of edits to already applied migration content. -3. More native lock backends and recovery/concurrency validation; three database families now have opt-in locks. -4. Repeatable migrations distinct from execution hooks. -5. SQLite native drop-column selection, generated columns, table options, sequence state and complex-index preservation beyond the currently guarded subset. -6. Complete imperative/fluent operation coverage and ownership-aware default/uniqueness cleanup. -7. Continued operation-level provider documentation and live test coverage. - -## Validation and maintenance - -The original master baseline (`ab3aa9f`) had 139 passing SQLite tests and one skipped default-removal test. At upgrade source `874cb88`, a rebuilt solution passed **83 unit tests and 160 SQLite tests, with no skips**. Provider PR #174 passed all eleven database/unit jobs and the coverage gate in [run 35729926918](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35729926918). New native-lock tests in #177 require their own live CI verification. - -This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; batch scripts, operation inventory, several provider ownership/metadata fixes and broader deployment regressions remain work in progress. Competitors were reviewed through documentation/source, **not executed in a comparative harness**. - -When updating: - -- Pin the new source revision and recheck SQLite rebuilds after refactoring. -- Verify competitor provider versions before promoting “Check” to a compatibility promise. -- Keep native SQL, automatic emulation and author-written workarounds distinct. -- Review ignored tests, schema round trips and real data, not just generated SQL. -- Update the date, sources and homepage summary together. - -## Source index - -- **Migrator:** [revision][m-revision], [runner][m-runner], [loader][m-loader], [execution][m-execution], [lifecycle][m-migration], [API][m-api], [history][m-provider], [factory][m-factory], [live tests][m-live], [SQLite implementation][m-sqlite], [SQLite model][m-sqlite-model]. -- **FluentMigrator:** [quick start][f-start], [configuration][f-config], [SQL][f-sql], [auto-reverse][f-reverse], [maintenance][f-maintenance], [profiles][f-profiles], pinned [SQLite generator][f-sqlite-generator] and [processor][f-sqlite-processor]. -- **EF Core:** [overview][ef-overview], [management][ef-managing], [deployment][ef-applying], [history][ef-history], [providers][ef-providers], [seeding][ef-seeding], [SQLite][ef-sqlite]. -- **DbUp:** [usage][d-usage], [providers][d-providers], [journal][d-journal], [script types][d-types], [transactions][d-transactions], [variables][d-variables], [logging][d-logging], [databases][d-databases], [philosophy][d-philosophy]. -- **Evolve:** [concepts][e-concepts], [options][e-options], [execution][e-start], [requirements][e-requirements]. -- **EF6:** [migrations][ef6-main], [automatic][ef6-auto], [history][ef6-history], [CLI][ef6-cli]. -- **grate / RoundhousE:** [grate][g-home], [options][g-config], [script types][g-types], [migration guide][g-migrate], [RoundhousE][r-home]. -- **SQLite engine:** [ALTER TABLE and reconstruction procedure][sqlite-alter]. - -[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Migrator.cs -[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/MigrationLoader.cs -[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/MigrationExecution.cs -[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Framework/Migration.cs -[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Framework/ITransformationProvider.cs -[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Providers/TransformationProvider.cs -[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/ProviderFactory.cs -[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/docs/live-database-tests.md -[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs -[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs -[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs -[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs -[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs -[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs -[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs -[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs -[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs -[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs -[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs -[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs -[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs -[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/874cb88c43a2134baa7a5355145ec16809b8c350/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs -[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/874cb88c43a2134baa7a5355145ec16809b8c350/ -[f-start]: https://fluentmigrator.github.io/intro/quick-start.html -[f-config]: https://fluentmigrator.github.io/intro/configuration.html -[f-sql]: https://fluentmigrator.github.io/operations/execute-sql.html -[f-reverse]: https://fluentmigrator.github.io/migration-types/auto-reversing.html -[f-maintenance]: https://fluentmigrator.github.io/migration-types/maintenance.html -[f-profiles]: https://fluentmigrator.github.io/migration-types/profiles.html -[f-sqlite-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteGenerator.cs -[f-sqlite-processor]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Processors/SQLite/SQLiteProcessor.cs -[ef-overview]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/ -[ef-managing]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing -[ef-applying]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying -[ef-history]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/history-table -[ef-providers]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers -[ef-seeding]: https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding -[ef-sqlite]: https://learn.microsoft.com/en-us/ef/core/providers/sqlite/limitations -[d-usage]: https://dbup.readthedocs.io/en/latest/usage/ -[d-providers]: https://dbup.readthedocs.io/en/latest/more-info/script-providers/ -[d-journal]: https://dbup.readthedocs.io/en/latest/more-info/journaling/ -[d-types]: https://dbup.readthedocs.io/en/latest/more-info/script-types/ -[d-transactions]: https://dbup.readthedocs.io/en/latest/more-info/transactions/ -[d-variables]: https://dbup.readthedocs.io/en/latest/more-info/variable-substitution/ -[d-logging]: https://dbup.readthedocs.io/en/latest/more-info/logging/ -[d-databases]: https://dbup.readthedocs.io/en/latest/supported-databases/ -[d-philosophy]: https://dbup.readthedocs.io/en/latest/philosophy-behind-dbup/ -[e-concepts]: https://evolve-db.netlify.app/concepts/ -[e-options]: https://evolve-db.netlify.app/configuration/options/ -[e-start]: https://evolve-db.netlify.app/getting-started/ -[e-requirements]: https://evolve-db.netlify.app/requirements/ -[ef6-main]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ -[ef6-auto]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/automatic -[ef6-history]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/history-customization -[ef6-cli]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ef6-exe -[g-home]: https://grate-devs.github.io/grate/ -[g-config]: https://grate-devs.github.io/grate/configuration-options/ -[g-types]: https://grate-devs.github.io/grate/script-types/ -[g-anytime]: https://grate-devs.github.io/grate/script-types/anytime/ -[g-everytime]: https://grate-devs.github.io/grate/script-types/everytime/ -[g-onetime]: https://grate-devs.github.io/grate/script-types/one-time/ -[g-migrate]: https://grate-devs.github.io/grate/migrating-from-roundhouse/ -[r-home]: https://github.com/chucknorris/roundhouse -[sqlite-alter]: https://www.sqlite.org/lang_altertable.html -[flyway-undo]: https://documentation.red-gate.com/flyway/reference/commands/undo -[flyway-baseline]: https://www.red-gate.com/hub/product-learning/flyway/flyways-baseline-migrations-explained-simply/ -[liquibase-rollback]: https://support.liquibase.com/hc/en-us/articles/29383086010523-How-to-Define-Rollbacks -[liquibase-preconditions]: https://docs.liquibase.com/community/user-guide-5-0-4/what-are-preconditions -[f-generic-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.Core/Generators/Generic/GenericGenerator.cs +# .NET database migration frameworks: detailed feature comparison + +**Reviewed: 22 September 2026.** This is a capability comparison, not a benchmark or an overall ranking. + +The main matrices cover **DotNetProjects.Migrator, FluentMigrator, EF Core migrations, DbUp and Evolve**—all five frameworks on the homepage. Additional sections cover **EF6, grate and RoundhousE**, with a short boundary comparison for **Flyway and Liquibase**. This is a defined shortlist, not a claim to catalogue every migration package ever published. + +Migrator findings are pinned to upgrade-stack commit [`8d8818e`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. + +[Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Project README](../README.md) · [SQLite emulation comparison](#sqlite-emulation-comparison) · [Source index](#source-index) + +## Contents + +- [How to read the matrices](#how-to-read-the-matrices) +- [Authoring and application integration](#authoring-and-application-integration) +- [Schema and data operations](#schema-and-data-operations) +- [History, ordering and repeatability](#history-ordering-and-repeatability) +- [Transactions, rollback and coordination](#transactions-rollback-and-coordination) +- [Deployment, inspection and configuration](#deployment-inspection-and-configuration) +- [Database coverage and portability](#database-coverage-and-portability) +- [SQLite emulation comparison](#sqlite-emulation-comparison) +- [EF6, grate and RoundhousE](#ef6-grate-and-roundhouse) +- [Flyway and Liquibase in a .NET deployment](#flyway-and-liquibase-in-a-net-deployment) +- [Choosing a framework and identifying Migrator gaps](#choosing-a-framework-and-identifying-migrator-gaps) +- [Validation and maintenance](#validation-and-maintenance) +- [Source index](#source-index) + +## How to read the matrices + +| Term | Meaning | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Built-in / named API | The reviewed tool provides this operation or workflow. Database restrictions still apply. | +| Configure | Available through documented runner settings, composition or extension points. | +| Custom | You supply application code, SQL or deployment orchestration. Not automatic framework behavior. | +| No built-in | No implementation in the inspected Migrator source, or no equivalent in the reviewed documented workflow. It does not rule out third-party extensions. | +| Provider-dependent | Availability or semantics depend on the database integration and release. | +| Not verified | Evidence is insufficient for a positive or negative compatibility claim. | + +A SQL runner can execute a hand-authored table rebuild; that does **not** mean it automatically emulates `AlterColumn`. Likewise, recording applied migrations is not schema-drift detection, a transaction is not a deployment mutex, and a version downgrade is not a data restore. + +## Authoring and application integration + +Evidence: [Migrator runner][m-runner], [loader][m-loader], [migration contract][m-migration]; [FluentMigrator quick start][f-start] and [SQL execution][f-sql]; [EF Core overview][ef-overview] and [managing migrations][ef-managing]; [DbUp usage][d-usage] and [script providers][d-providers]; [Evolve concepts][e-concepts] and [configuration][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| ---------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------- | ------------------------------------- | ------------------------------------------ | +| Primary authoring artifact | Public C# migration class | C# migration class with fluent expressions | Generated, editable C# migration + model snapshot | SQL file or C# `IScript` | Versioned SQL file | +| Requires an ORM model | No | No | Yes, for normal scaffolding | No | No | +| Generates changes from model differences | No built-in | No built-in model differ in core workflow | Yes | No; author scripts | No; author scripts | +| Migration without a model change | Yes | Yes | Empty migration, then custom operations | Yes | Yes | +| Schema DSL / transformation API | Imperative API and structured `MigrationBuilder`; provider limits apply | Fluent create/alter/delete expressions | `MigrationBuilder` operations | No schema DSL; SQL / commands | No schema DSL; SQL | +| Custom C# logic | `Up` / `Down`; open provider | Migration code / connection operations | SQL/custom operations for database work | `IScript` and command factory | Surrounding host logic; migrations are SQL | +| Raw SQL | Command, query and scalar APIs | Inline, file and embedded SQL | `migrationBuilder.Sql` | Primary workflow | Primary workflow | +| Migration discovery | Assembly scan or explicit `Type[]` | Assembly scanning / filters | Context's migration assembly | Configurable script providers | Locations or embedded resources | +| Constructor dependency injection | Optional Microsoft DI/options package; custom activator supported | Runner/DI integration | Context services; migration customization is separate | Custom script provider/host if needed | No C# migration constructors | +| Embedded execution | Yes | Yes | Yes | Yes | Library mode | +| Dedicated execution host | Library or source-built packaged .NET tool (unreleased) | Library or packaged runner | Tooling, bundles or custom host | Write your own | CLI, .NET tool or library | + +EF Core's model snapshot comparison is not a live-database schema comparison. DbUp's C# support is more than static SQL file loading, but it does not supply a cross-database schema-operation layer. + +## Schema and data operations + +This table separates having an authoring API from that API working identically on every engine. SQLite is broken out below. Evidence: [Migrator interface][m-api] and [provider factory][m-factory]; [FluentMigrator operations][f-start]; [EF Core migration operations][ef-managing]; [DbUp script execution][d-usage]; [Evolve SQL model][e-concepts]. + +| Operation family | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| --------------------------------------- | ---------------------------------------- | ----------------------------------- | ------------------------------------------ | ----------------------------- | ------------------------------------- | +| Create / drop table | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | +| Rename table | Schema API | Fluent API | Migration operation | Author SQL | Author SQL | +| Add / drop / rename column | Schema API | Fluent API | Migration operations | Author SQL | Author SQL | +| Change type / nullability / default | `ChangeColumn` and default API | Alter expressions | `AlterColumn` | Author SQL | Author SQL | +| Primary / composite keys | API; provider-dependent | Fluent expressions | Migration operations | Author SQL | Author SQL | +| Foreign keys / delete behavior | API; mapped constraint types | Fluent expressions | Migration operations | Author SQL | Author SQL | +| Unique constraints | API | Fluent expressions | Migration operations | Author SQL | Author SQL | +| Check constraints | API using SQL predicate | Provider/custom SQL as applicable | Migration operations | Author SQL | Author SQL | +| Indexes | API and index model | Fluent expressions | Operations / provider annotations | Author SQL | Author SQL | +| Filtered / included / clustered indexes | Provider-specific subsets | Provider-specific options | Provider-specific support | Engine-specific SQL | Engine-specific SQL | +| Views | `AddView` and SQL | Usually SQL | Usually SQL migrations | Author SQL | SQL; repeatables useful | +| Stored procedures / triggers | Raw SQL | SQL / connection operations | SQL / custom operations | Author SQL | Author SQL | +| Fixed-data insert / update / delete | Data API | Fluent data expressions | `InsertData` / `UpdateData` / `DeleteData` | SQL or C# | SQL | +| Transform existing data | SQL, provider reads/writes, copy helpers | SQL / connection operations | SQL / custom operations | SQL or C# | SQL | +| Live table / column existence | Existence and metadata APIs | Schema query API | SQL/custom code | SQL or C# | SQL | +| Full schema-drift report | No built-in | Not established by version tracking | Snapshot comparison alone is insufficient | Journal alone is insufficient | Checksums concern scripts, not schema | + +## History, ordering and repeatability + +Evidence: [Migrator loader][m-loader], [execution][m-execution] and [history storage][m-provider]; [FluentMigrator configuration][f-config], [maintenance][f-maintenance] and [profiles][f-profiles]; [EF Core overview][ef-overview], [history][ef-history] and [seeding][ef-seeding]; [DbUp journaling][d-journal], [script types][d-types] and [usage][d-usage]; [Evolve concepts][e-concepts] and [options][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| -------------------------- | ------------------------------------------------------- | -------------------------------------- | ---------------------------------------------- | ------------------------------------------------ | ------------------------------- | +| Applied-change identity | Numeric version within scope | Migration version | Migration ID | Script name | Script metadata | +| Default history | `SchemaInfo` | Version table | `__EFMigrationsHistory` | E.g. `SchemaVersions` | `changelog` | +| History customization | Table name; scope column | Version-table metadata | Table/schema; custom services | Custom journal / table | Metadata table/schema | +| Independent modules | Scope + selected migrations | Separate history + filters | Contexts/assemblies + separate history | Filters + separate journals | Locations + separate metadata | +| Environment selection | Tags with explicit Any/All matching; scopes and named profiles | Tags / profiles / configuration | Context/deployment configuration | Filters / host | Locations / placeholders / host | +| Skip applied work | Version history | Version history | Migration history | Journal | Metadata | +| Applied-source checksum | No built-in | Not a core version-table guarantee | No script checksum journal | Standard journal tracks names; custom validation | Script checksums | +| Late lower-numbered change | Revisits missing versions up to target | Check runner policy | Do not assume IDs make diverging branches safe | Unrecorded scripts eligible; ordering matters | `OutOfOrder` | +| Repeat on content change | Custom | Not equivalent to maintenance/profiles | Not equivalent to seeding | Custom checksum-aware runner | Repeatable SQL | +| Always-run work | Ordered before/after-run and before/after-migration stages; selected profiles | Maintenance / selected profiles | Seeding APIs, EF 9+ | `RunAlways` / `NullJournal` | Not identical to RunAlways | +| Existing-schema baseline | Custom verified history initialization | Custom baseline/runner strategy | Existing-schema workflow | `MarkAsExecuted` | `StartVersion` / skip options | +| Repair checksums | Not applicable | Not established by version history | Not applicable | Custom journal concern | `repair` | + +**Migrator scope detail:** unscoped migrations inherit the runner scope. Explicitly scoped migrations are selected only for that scope; duplicate validation and history access use the same effective scope. Custom legacy providers without `IMigrationHistory` retain their prior behavior. History isolation is not table isolation. [Loader][m-loader], [execution][m-execution], [provider][m-provider]. + +## Transactions, rollback and coordination + +Evidence: [Migrator execution][m-execution] and [runner][m-runner]; [FluentMigrator configuration][f-config] and [auto-reverse][f-reverse]; [EF Core management][ef-managing], [deployment][ef-applying] and [SQLite limitations][ef-sqlite]; [DbUp transactions][d-transactions] and [philosophy][d-philosophy]; [Evolve concepts][e-concepts] and [options][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| ------------------------------ | -------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------ | +| Default transaction unit | Per migration | Per migration; configurable | Version-sensitive: EF 9 grouped pending migrations, reverted in EF 10 | None | Per migration | +| Whole-run transaction | `WholeSession` for verified SQLite, PostgreSQL and SQL Server dialects | Configure/orchestrate; check runner | Depends on version/operations | `WithTransaction()` | `CommitAll` | +| Per-change transaction opt-out | Run-level `None`; no per-migration transaction attribute | Transaction behavior | Raw SQL suppression | Choose strategy / separate runs | Script opt-out | +| Failed DDL rollback | Engine-dependent | Engine-dependent | Engine-dependent | When enabled and supported | Engine-dependent | +| Reverse committed migration | Authored `Down()` | `Down()` | Generated/editable `Down()` | Custom undo / forward fix | Forward fix; no Down command | +| Generate reverse operations | Supported create/rename operations; explicit reverse required for destructive/data/SQL operations | Supported auto-reverse expressions | Scaffolding; review output | No schema reverse generator | No | +| Target earlier version | `MigrateTo` | Down/rollback APIs | Earlier target / reverse script | Custom | Target limits forward work, not undo | +| Restore deleted data | Backup / reconstruction | Same | Same | Same | Same | +| Cross-process coordination | Opt-in native session locks for SQL Server, PostgreSQL and MySQL/MariaDB; custom abstraction | Serialize deployment / application-lock pattern | Migration locking, EF 9+; execution-path dependent | Host/provider concern; journal is not a lock | Cluster setting; provider-dependent | +| Post-commit hooks | `AfterUp` / `AfterDown` | Maintenance stages | Host/seeding lifecycle; not direct equivalent | Host / ordered scripts | Host / ordered scripts | + +A scope, checksum, history primary key or ordinary database write lock does not prove that two deployments can safely run the entire sequence concurrently. Evolve's cluster setting must be checked for the selected provider; it is not a blanket SQLite session-lock guarantee. + +## Deployment, inspection and configuration + +Evidence: [Migrator runner][m-runner] and [execution][m-execution]; [FluentMigrator runners][f-start] and [configuration][f-config]; [EF Core deployment][ef-applying]; [DbUp usage][d-usage], [variables][d-variables] and [logging][d-logging]; [Evolve execution][e-start] and [options][e-options]. + +| Capability | Migrator | FluentMigrator | EF Core | DbUp | Evolve | +| ------------------------------------ | ------------------------------------------------- | ------------------------------------------ | --------------------------------- | -------------------------------- | ------------------------------------------------ | +| Packaged CLI | Source project `DotNetProjects.Migrator.Tool`; not published by this upgrade | Yes | `dotnet ef` | Core library; custom host | Yes | +| Dedicated migration bundle generator | No; publish host | Package runner/migrations | Yes | Publish host | CLI distribution, not EF-style bundle generation | +| Review SQL without applying | Connected/offline structured subset; unsupported operations fail explicitly | Preview/output | Scripts | Authored SQL / pending scripts | Authored SQL | +| Dry-run qualification | `DryRun` plans versions without migration bodies, callbacks, transactions or history creation | Processor preview; user code needs care | Not a full side-effect simulation | Pending list / custom simulation | `RollbackAll` actually executes | +| Idempotent deployment SQL | Custom | Preview is not idempotent history guarding | Provider-dependent; not SQLite | Author SQL / use journal | Author SQL / use metadata | +| Status | Versions / loaded types | Runner/tool info | CLI / history APIs | Pending/executed APIs | `info` | +| Command timeout | Provider setting | Processor setting | Database/provider setting | Runner/provider setting | `CommandTimeout` | +| Logging | Legacy logger plus optional Microsoft logging adapter (SQL/exception details omitted) | Logging integration | EF logging | `IUpgradeLog` / integrations | Host/CLI | +| SQL substitution | Custom | Script tokens | Custom logic | `$variable$` | `${placeholder}` | +| Deployment identity | Host connection | Runner connection | Migration connection | Host connection | Tool connection | + +**Migrator dry run is not an offline SQL preview.** Execution starts provider work while `Up()`/`Down()` are skipped. It cannot show SQL from those skipped bodies and should not be described as side-effect-free database validation. [Execution source][m-execution]. + +## Database coverage and portability + +| Framework | How support is supplied | What it does not guarantee | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| Migrator | Source dialects + separate ADO.NET drivers. Live CI covers SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix and Sybase; Ingres is another source dialect. [CI guide][m-live]. | Every server/driver release, operation or arbitrary SQL construct. | +| FluentMigrator | Provider generators/processors. [Configuration][f-config]. | The same expression working on every engine. | +| EF Core | Relational provider packages. [Multiple providers][ef-providers]. | One provider's generated migrations working unchanged elsewhere. | +| DbUp | Database integrations. [Provider list][d-databases]. | SQL dialect translation. | +| Evolve | Database integrations. [Requirements][e-requirements]. | SQL translation or identical transactions. | + +A migration can compile yet require a table copy, lose an unsupported schema detail or fail on existing data. Compare the exact operation and data shape, not just database names. + +## SQLite emulation comparison + +### What emulation means + +SQLite has native table rename, column rename, add-column and (on sufficiently recent engines, subject to restrictions) drop-column operations. SQLite 3.53.0 added native `ALTER COLUMN … SET/DROP NOT NULL`; it still does not provide general type/default alteration or `ALTER TABLE ADD/DROP CONSTRAINT`. More complex changes require a replacement table, copying rows and rebuilding dependent objects. Native capabilities evolve independently of the .NET driver package. [SQLite ALTER TABLE reference][sqlite-alter]. + +Migrator reads the **live schema** into `SQLiteTableInfo`, modifies that representation and calls `RecreateTable`. It creates `
Temp`, copies mapped columns with `INSERT … SELECT`, drops the original, renames the replacement and recreates represented indexes. This works without an ORM model, but depends on what its schema reader can represent. [Implementation][m-sqlite], [schema model][m-sqlite-model]. + +### Automatic operation matrix + +**R** = built-in rebuild; **N** = native SQL path, subject to engine restrictions; **U** = unique-index substitution; **Manual** = author the change/rebuild yourself; **Manual** also covers a generated statement that the engine does not support. Rows describe **changes to an existing table**, not constraints declared when creating it. + +The combined SQL-runner column applies **individually to DbUp and Evolve**: both execute supplied SQL rather than diffing/rebuilding the schema. grate and RoundhousE follow the same distinction. Manual does not mean the engine cannot perform the operation. + +Evidence: [EF Core SQLite operation table][ef-sqlite], [FluentMigrator SQLite generator][f-sqlite-generator], [inherited SQL templates][f-generic-generator] and [processor][f-sqlite-processor], [DbUp scripts][d-usage], [Evolve concepts][e-concepts]. Migrator cells are supported by the source/test inventory below. + +| Existing-table operation | Migrator | FluentMigrator | EF Core | DbUp / Evolve | +| ---------------------------- | --------------------------------- | ------------------------------- | ----------- | ---------------------------------------------- | +| Add ordinary column | R | N | N | Manual SQL | +| Remove column | N on SQLite 3.35+ when eligible; R fallback | N; engine restrictions | R | Manual SQL/rebuild | +| Rename column | N on SQLite 3.26+; R fallback | N; engine restrictions | N | Manual SQL/rebuild | +| Change declared type | R | Manual | R | Manual rebuild | +| Change nullability | R | Manual | R | Manual SQL on 3.53+ / rebuild on older engines | +| Change default | R via full `Column` | Manual | R via alter | Manual rebuild | +| Remove default | R via dedicated API; caveat below | Manual | R via alter | Manual rebuild | +| Add primary key | R | Manual | R | Manual rebuild | +| Remove primary key | R | Manual | R | Manual rebuild | +| Add foreign key | R | Manual | R | Manual rebuild | +| Remove foreign key | R | Manual | R | Manual rebuild | +| Add unique constraint | R | U | R | Manual rebuild/index | +| Remove unique constraint | R | U for tool-created unique index | R | Manual rebuild/index | +| Add check constraint | R | Manual | R | Manual rebuild | +| Remove check constraint | R | Manual | R | Manual rebuild | +| Create / drop ordinary index | N | N | N | Manual SQL | +| Rename table | N | N | N | Manual SQL | + +The table describes framework paths, not everything the newest SQLite engine can do. Migrator still rebuilds for nullability changes; FluentMigrator still rejects its general alter-column expression even when a newer engine can execute a hand-authored NOT NULL alteration. + +EF Core rebuilds rely on model-represented artifacts; the docs identify failures for artifacts outside that model. EF 9+ uses a SQLite lock table with abandoned-lock recovery considerations. These are separate from rebuild support. [SQLite limitations][ef-sqlite]. + +FluentMigrator supports inline FKs during table creation. Its reviewed generator directs callers to manual reconstruction for later FK changes; `LOOSE` mode skips unsupported expressions rather than emulating them. Unique-index substitution does not imply that an existing table-level UNIQUE constraint can be dropped as an index. [Generator][f-sqlite-generator]. + +### Migrator's emulated operations, precisely + +Methods refer to the pinned [SQLite provider][m-sqlite]. Tests illustrate evidence, not exhaustive coverage of every data/schema combination. + +| API / operation | Implementation behavior | Qualification / evidence | +| ------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `AddColumn` | Adds a column and mapping without an old source column; rebuilds. | Existing rows receive SQLite default/NULL behavior; incompatible NOT NULL requirements can fail. [Tests][t-add-column]. | +| `ChangeColumn` | Replaces the entire matching `Column` definition; rebuilds. | Specify properties to retain. Type affinity during copying is not arbitrary data conversion. [Tests][t-change-column]. | +| `RemoveColumnDefaultValue` | Clears parsed default; rebuilds. Generic default-removal regression is enabled and passes. | Dedicated and generic default-removal regressions run; provider CI is required for changes. [Tests][t-sqlite-general]. | +| `RemoveColumn` | Uses native DROP COLUMN on SQLite 3.35+ for eligible columns; otherwise removes represented dependencies and rebuilds. | Rejects detected CHECK references and composite dependencies until adjusted. Can remove inbound single-column FKs from other tables. [Tests][t-remove-column]. | +| `RenameColumn` | Native on SQLite 3.26+; reconstruction fallback for older engines. | Native rename delegates dependency rewriting to SQLite; reconstruction is not an arbitrary SQL-expression rewriter. [Tests][t-rename-column]. | +| `AddPrimaryKey` | Sets membership, orders selected columns, rebuilds. | Composite keys supported; `PrimaryKeyExists` checks for any PK rather than matching its name. [Tests][t-pk]. | +| `RemovePrimaryKey` | Clears PK/PK-identity flags; rebuilds. | Changes identity-related semantics; review referencing tables. [Source][m-sqlite]. | +| `AddForeignKey` / `RemoveForeignKey` | Adds/removes represented FK; rebuilds child table. | Validate existing rows and enforcement. [FK tests][t-fk], [integrity tests][t-integrity]. | +| `AddUniqueConstraint` | Adds named unique definition; rebuilds. | Duplicate data can reject the copy. [Metadata tests][t-uniques]. | +| `AddCheckConstraint` | Adds named CHECK SQL; rebuilds. | Predicate must accept existing rows and be understood by the reader. [Tests][t-check]. | +| `RemoveConstraint` | Removes matching unique and check definitions; rebuilds. | Does not remove FKs/PKs; use dedicated APIs. [Source][m-sqlite]. | +| `RemoveAllConstraints` | Clears PK, unique, FK and CHECK definitions before rebuilding. | Constraint removal can fail when dependent schemas/data require a coordinated migration. [Tests][t-remove-constraints], [source][m-sqlite]. | +| `RemoveAllIndexes` | Clears indexes **and unique constraints**; rebuilds. | Broader than dropping non-unique indexes. [Source][m-sqlite]. | +| `RecreateTable` | Public low-level schema/mapping reconstruction. | Requires a consistent supported representation. [Composite-key round-trip test][t-recreate]. | +| `TruncateTable` | Emits `DELETE FROM`. | Not native TRUNCATE and not an identity-sequence reset. [Source][m-sqlite]. | + +### What survives reconstruction—and what is not guaranteed + +| Schema/data detail | Migrator at the pinned revision | Implication | +| ---------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | +| Mapped rows | Named-column `INSERT … SELECT`. | New constraints/types must accept the data. | +| Names, parsed types, nullability, defaults | Included in column model. | Not a lossless representation of arbitrary CREATE SQL. | +| Composite PKs | Represented; dedicated rebuild test. | Check membership/order when replacing definitions. | +| FKs and delete actions | Read from schema/PRAGMA; emitted into replacement DDL. | Not a promise about every clause, e.g. arbitrary deferrability. | +| Unique / CHECK definitions | Included in `SQLiteTableInfo`. | Reader restrictions apply; rename does not rewrite arbitrary CHECK expressions. | +| Indexes / represented filters | Recreated after replacement. | Complex predicates, expressions, collations and sort details require separate verification. | +| Triggers | Collected and replayed for supported rebuilds without renames; unsafe rename fallback rejected. | Trigger SQL is replayed only where the rebuild does not require rewriting its identifiers. | +| Views / dependent SQL | No general dependency-SQL rewrite. | Validate/recreate dependencies after renames/drops. | +| `WITHOUT ROWID`, `STRICT`, generated columns | Unsupported reconstruction is rejected before dropping the original. | No preservation claim for unsupported external table properties. | +| Hidden `rowid` / AUTOINCREMENT high-water mark | Mapped columns and retained AUTOINCREMENT high-water state are preserved; hidden rowid is not mapped. | Deleted historical identity values are not reused after a rebuild; hidden rowid values may change. | +| Type / length enforcement | Changes declarations, not SQLite typing rules. | Declared size is not SQL Server-like length enforcement. | +| FK enforcement state | Runner and owned rebuild transactions restore the prior setting after success/failure. | Caller-owned active SQLite transactions require FK settings to be configured before beginning the transaction. | +| Whole-database FK validation | Runner and owned rebuild transactions validate integrity before commit. | Enabling enforcement alone does not validate existing rows. | + +Evidence: [SQLite provider][m-sqlite], [schema model][m-sqlite-model], [execution][m-execution], [SQLite reconstruction procedure][sqlite-alter]. Native drop-column selection and AUTOINCREMENT high-water preservation have regressions. Arbitrary dependency rewriting remains unsupported. + +### How the other frameworks compare on preservation + +| Framework | Replacement schema source | Responsibility for unsupported dependencies | +| ------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| Migrator | Live reader + `SQLiteTableInfo`. | Author handles objects outside the representation. | +| EF Core | Model/migration metadata. | Author handles artifacts outside automatic model-based rebuilding. [Docs][ef-sqlite]. | +| FluentMigrator | No general rebuild engine found in inspected SQLite components. | Author writes reconstruction for unsupported alterations. [Generator][f-sqlite-generator], [processor][f-sqlite-processor]. | +| DbUp | Project SQL / C#. | Script author. [Usage][d-usage]. | +| Evolve | Project SQL. | Script author. [Concepts][e-concepts]. | +| grate / RoundhousE | Project SQL. | Script author. Database integration is not emulation. [grate][g-home], [RoundhousE][r-home]. | +| EF6 | Selected provider's migration generator. | Provider-specific; EF Core rebuild support must not be attributed to EF6. No specific EF6 SQLite emulation verified here. | + +**Practical conclusion:** Migrator's differentiator is live-schema-based SQLite reconstruction without an ORM model. It is not unique in automatic SQLite rebuilding—EF Core also does this—and is not a lossless rewriter of every SQLite schema feature. + +## EF6, grate and RoundhousE + +Evidence: [EF6 migrations][ef6-main], [automatic migrations][ef6-auto], [history][ef6-history], [CLI][ef6-cli]; [grate home][g-home], [configuration][g-config], [script types][g-types], [anytime][g-anytime], [everytime][g-everytime], [one-time][g-onetime]; [RoundhousE][r-home] and [grate migration guide][g-migrate]. + +| Capability | EF6 Code First | grate | RoundhousE | +| ------------------------------------------- | --------------------------------------------- | --------------------------------- | ----------------------------------------------- | +| Authoring | C# from EF6 model | Lifecycle SQL folders | Lifecycle SQL folders | +| ORM dependency | EF6 model/context | None | None | +| Model-difference generation | Yes | No | No | +| Automatic migrations without explicit files | Optional EF6 feature | No | No | +| Reverse version | `Down`, target migration | Forward/custom recovery | Forward/custom recovery | +| Change once | Versioned migration | One-time scripts | One-time scripts | +| Run after content change | Not a SQL repeatable mechanism | Anytime scripts | Anytime workflow | +| Every deployment | Seed/custom lifecycle | Everytime scripts | Everytime workflow | +| Detect script edits | Not a script checksum journal | One-time hash checking | Changed-script policies | +| Existing-schema baseline | Existing-schema workflow | `--baseline` | Verify release's workflow | +| Transactions | EF/provider execution | Opt-in `--transaction` | Transaction flags / outside-transaction scripts | +| Environment filtering | Host/configuration | Filename conventions | Environment scripts | +| SQL token replacement | Custom | User tokens | Tokens | +| History separation | Context history / customization | Migration schema/configuration | Repository/schema conventions | +| Preview / inspection | Script generation | `--dryrun`, logs | Check release's dry-run/log tooling | +| Execution | PMC/runtime; `ef6.exe` replaces `migrate.exe` | CLI; self-contained distributions | CLI / .NET tooling | +| Automatic SQLite emulation | Provider-specific; not verified | None in documented workflow | None in documented workflow | + +RoundhousE maintainers point to grate as a successor. The migration guide documents differences; do not assume parity for every flag, history configuration or folder. This is a compatibility consideration, not a claim of identical release/support status. + +## Flyway and Liquibase in a .NET deployment + +These can migrate databases used by .NET applications, but do not replace Migrator's in-process C# transformation API directly. This narrower comparison avoids folding edition-dependent features into the main matrices. + +| Concern | Flyway | Liquibase | +| ------------------------- | ----------------------------------------------------------------- | --------------------------------------------------- | +| Artifacts | Versioned / repeatable migrations | Changelog changesets, including formatted SQL | +| Recovery | Explicit undo migrations where the selected edition supports Undo | Change-type-dependent / authored rollback | +| Selection and assumptions | Tool configuration; check command/edition | Contexts/preconditions; format/version restrictions | +| Automatic SQLite rebuild | Not established here; supplied SQL is not emulation | Not established here; verify change type/extension | +| .NET integration | Separate deployment tool | Separate deployment tool | + +Sources: [Flyway Undo][flyway-undo], [baseline migrations][flyway-baseline], [Liquibase rollback][liquibase-rollback], [preconditions][liquibase-preconditions]. This document does not claim that every command is available in a free edition. + +## Choosing a framework and identifying Migrator gaps + +These interpretations are grounded in the preceding evidence, rather than universal recommendations. + +| Requirement | Candidate / tradeoff | +| ---------------------------------------------- | ----------------------------------------------------------------------- | +| No ORM model, frequent SQLite alterations | Evaluate Migrator's live-schema reconstruction and preservation limits. | +| EF model defines schema | EF Core supplies scaffolding, rebuilds and deployment artifacts. | +| Handwritten C# / packaged runners / fluent DSL | FluentMigrator; manual work for unsupported SQLite alterations. | +| SQL-first runner composed in .NET | DbUp's script providers, journal and transaction strategies. | +| SQL checksums / change-triggered repeatables | Evolve's built-in conventions. | +| Existing RoundhousE folders | Evaluate grate's migration guide and history compatibility. | +| Existing EF6 application | Assess EF6/provider behavior separately from EF Core. | +| Multi-language database-owned deployment | Evaluate Flyway/Liquibase and required editions. | + +Potential Migrator improvements, **not implemented-feature claims**: + +1. Broader structured SQL-preview coverage, provider-specific batch scripts and CLI deployment validation. The source CLI and preview subset already exist. +2. Validation of edits to already applied migration content. +3. More native lock backends and recovery/concurrency validation; three database families now have opt-in locks. +4. Repeatable migrations distinct from execution hooks. +5. SQLite generated columns, table options, hidden rowid and complex-index preservation beyond the currently guarded subset. +6. Broader behavioral parity tests beyond the [fluent method-family inventory](fluent-operation-coverage.md), and safe migration of historical uniqueness objects without ownership markers. +7. Continued operation-level provider documentation and live test coverage. + +## Validation and maintenance + +The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. At upgrade source `8d8818e`, a rebuilt solution passed **89 unit tests and 173 SQLite tests, with no skips**. The earlier tooling source `c0a7378` passed all eleven database/unit jobs and the coverage gate in [run 35733769486](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35733769486), including native lock tests on SQL Server, PostgreSQL, MySQL and MariaDB. Later changes require their own PR checks; a green earlier revision is not evidence for a later revision. + +This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; provider-specific batch scripts, several metadata/legacy ownership fixes and broader deployment regressions remain work in progress. The operation inventory maps normal API method families to fluent/context entry points, but does not establish every overload/provider combination through execution. Competitors were reviewed through documentation/source, **not executed in a comparative harness**. + +When updating: + +- Pin the new source revision and recheck SQLite rebuilds after refactoring. +- Verify competitor provider versions before promoting “Check” to a compatibility promise. +- Keep native SQL, automatic emulation and author-written workarounds distinct. +- Review ignored tests, schema round trips and real data, not just generated SQL. +- Update the date, sources and homepage summary together. + +## Source index + +- **Migrator:** [revision][m-revision], [runner][m-runner], [loader][m-loader], [execution][m-execution], [lifecycle][m-migration], [API][m-api], [history][m-provider], [factory][m-factory], [live tests][m-live], [SQLite implementation][m-sqlite], [SQLite model][m-sqlite-model]. +- **FluentMigrator:** [quick start][f-start], [configuration][f-config], [SQL][f-sql], [auto-reverse][f-reverse], [maintenance][f-maintenance], [profiles][f-profiles], pinned [SQLite generator][f-sqlite-generator] and [processor][f-sqlite-processor]. +- **EF Core:** [overview][ef-overview], [management][ef-managing], [deployment][ef-applying], [history][ef-history], [providers][ef-providers], [seeding][ef-seeding], [SQLite][ef-sqlite]. +- **DbUp:** [usage][d-usage], [providers][d-providers], [journal][d-journal], [script types][d-types], [transactions][d-transactions], [variables][d-variables], [logging][d-logging], [databases][d-databases], [philosophy][d-philosophy]. +- **Evolve:** [concepts][e-concepts], [options][e-options], [execution][e-start], [requirements][e-requirements]. +- **EF6:** [migrations][ef6-main], [automatic][ef6-auto], [history][ef6-history], [CLI][ef6-cli]. +- **grate / RoundhousE:** [grate][g-home], [options][g-config], [script types][g-types], [migration guide][g-migrate], [RoundhousE][r-home]. +- **SQLite engine:** [ALTER TABLE and reconstruction procedure][sqlite-alter]. + +[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Migrator.cs +[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/MigrationLoader.cs +[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/MigrationExecution.cs +[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Framework/Migration.cs +[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Framework/ITransformationProvider.cs +[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Providers/TransformationProvider.cs +[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/ProviderFactory.cs +[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/docs/live-database-tests.md +[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs +[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/8d8818eba926cddbe8e30179f3bfab79ad03bde6/ +[f-start]: https://fluentmigrator.github.io/intro/quick-start.html +[f-config]: https://fluentmigrator.github.io/intro/configuration.html +[f-sql]: https://fluentmigrator.github.io/operations/execute-sql.html +[f-reverse]: https://fluentmigrator.github.io/migration-types/auto-reversing.html +[f-maintenance]: https://fluentmigrator.github.io/migration-types/maintenance.html +[f-profiles]: https://fluentmigrator.github.io/migration-types/profiles.html +[f-sqlite-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Generators/SQLite/SQLiteGenerator.cs +[f-sqlite-processor]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.SQLite/Processors/SQLite/SQLiteProcessor.cs +[ef-overview]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/ +[ef-managing]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/managing +[ef-applying]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying +[ef-history]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/history-table +[ef-providers]: https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/providers +[ef-seeding]: https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding +[ef-sqlite]: https://learn.microsoft.com/en-us/ef/core/providers/sqlite/limitations +[d-usage]: https://dbup.readthedocs.io/en/latest/usage/ +[d-providers]: https://dbup.readthedocs.io/en/latest/more-info/script-providers/ +[d-journal]: https://dbup.readthedocs.io/en/latest/more-info/journaling/ +[d-types]: https://dbup.readthedocs.io/en/latest/more-info/script-types/ +[d-transactions]: https://dbup.readthedocs.io/en/latest/more-info/transactions/ +[d-variables]: https://dbup.readthedocs.io/en/latest/more-info/variable-substitution/ +[d-logging]: https://dbup.readthedocs.io/en/latest/more-info/logging/ +[d-databases]: https://dbup.readthedocs.io/en/latest/supported-databases/ +[d-philosophy]: https://dbup.readthedocs.io/en/latest/philosophy-behind-dbup/ +[e-concepts]: https://evolve-db.netlify.app/concepts/ +[e-options]: https://evolve-db.netlify.app/configuration/options/ +[e-start]: https://evolve-db.netlify.app/getting-started/ +[e-requirements]: https://evolve-db.netlify.app/requirements/ +[ef6-main]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ +[ef6-auto]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/automatic +[ef6-history]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/history-customization +[ef6-cli]: https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/ef6-exe +[g-home]: https://grate-devs.github.io/grate/ +[g-config]: https://grate-devs.github.io/grate/configuration-options/ +[g-types]: https://grate-devs.github.io/grate/script-types/ +[g-anytime]: https://grate-devs.github.io/grate/script-types/anytime/ +[g-everytime]: https://grate-devs.github.io/grate/script-types/everytime/ +[g-onetime]: https://grate-devs.github.io/grate/script-types/one-time/ +[g-migrate]: https://grate-devs.github.io/grate/migrating-from-roundhouse/ +[r-home]: https://github.com/chucknorris/roundhouse +[sqlite-alter]: https://www.sqlite.org/lang_altertable.html +[flyway-undo]: https://documentation.red-gate.com/flyway/reference/commands/undo +[flyway-baseline]: https://www.red-gate.com/hub/product-learning/flyway/flyways-baseline-migrations-explained-simply/ +[liquibase-rollback]: https://support.liquibase.com/hc/en-us/articles/29383086010523-How-to-Define-Rollbacks +[liquibase-preconditions]: https://docs.liquibase.com/community/user-guide-5-0-4/what-are-preconditions +[f-generic-generator]: https://github.com/fluentmigrator/fluentmigrator/blob/2e0acdb7c375b03e50e65f34ddf50e44ee45df30/src/FluentMigrator.Runner.Core/Generators/Generic/GenericGenerator.cs diff --git a/docs/runner-guide.md b/docs/runner-guide.md index 8c7c423a..ccb5b544 100644 --- a/docs/runner-guide.md +++ b/docs/runner-guide.md @@ -1,100 +1,100 @@ -# Runner and fluent API upgrade - -These APIs describe the source upgrade under review in PRs #173, #174, #175 and #177. They are not a statement about the currently released NuGet packages. Build the repository to try them; no package publication is part of this change. - -## Fluent quick start - -The [compiled quick-start project](../examples/FluentQuickStart/Program.cs) executes preview, migration and automatic reversal against SQLite: - -```sh -dotnet run --project examples/FluentQuickStart -``` - -```csharp -[Migration(1, Scope = "demo"), Tags("core")] -public class CreateUsers : AutoReversingMigration -{ - public override void BuildUp(MigrationBuilder migration) - { - migration.Create.Table("Users") - .WithColumn("Id").AsInt32().PrimaryKey() - .WithColumn("Name").AsString(255).NotNullable(); - } -} -``` - -Use `DotNetProjects.Migrator`, `.Framework` and `.Framework.Fluent`. A table definition is completed before execution. Existing imperative `Migration.Up/Down` classes keep working. `FluentMigration` supports authored `BuildDown`; `AutoReversingMigration` reverses supported create/rename operations in reverse order. Destructive changes, data, SQL and callbacks need explicit reverse operations. Automatic reversal never restores deleted data. - -The builder has `Create`, `Alter`, `Delete`, `Rename`, `Insert`, `Update`, `Execute` and `Administration`. Schema inspection is exposed through `FluentMigration.Schema`, and the provider through `Context`. History and transaction methods remain explicit context operations. Some administrative/data-copy operations use provider callbacks and cannot generate SQL previews. - -## Runner options - -`runner.Options` supports: - -| Option | Semantics | -| --- | --- | -| `Tags` / `TagMatch` | Ordinal names; explicit `Any` or `All`. No filter selects all versioned migrations. Filtered applied versions remain applied on downgrade. | -| `Profiles` | Explicit names of `[Profile("name")]` classes. Run after versioned migrations without recording versions; run again when selected again. | -| `TransactionMode` | `PerMigration` by default; `None` or `WholeSession` available. | -| `Activator` | Optional constructor activation delegate. | -| `Lock` / `LockTimeout` | Optional `IMigrationLock` lease; acquire before reading history and release on completion/failure. | - -Unscoped migrations inherit the provider scope; explicitly scoped migrations run only in that scope. Discovery, duplicate validation and history reads use the effective scope. Scopes separate history, not tables. Legacy custom providers can adopt the additive `IMigrationHistory` interface for read-only planning and effective-scope selection. - -Maintenance classes use `[Maintenance(MaintenanceStage.BeforeRun)]`, `BeforeMigration`, `AfterMigration` or `AfterRun`. Profiles and maintenance accept `Order` and `Scope`. Ordering uses `Order` then ordinal full type name. Hooks stop on failure; later hooks are not cleanup guarantees. Connection/transaction restoration and lock release do not depend on hooks running. Profiles and maintenance use `Up`; they do not acquire version records. - -## Transactions and locks - -`PerMigration` commits each successful migration. `None` leaves transaction behavior to the provider/operations. `WholeSession` is accepted for SQLite, PostgreSQL and SQL Server dialects; history-table initialization occurs before that transaction. Other dialects fail explicitly because transactional DDL has not been verified. Arbitrary imperative SQL can still violate transaction assumptions; database administration and implicit-commit statements require separate runs. - -`AfterUp`/`AfterDown` run after commit. In whole-session mode they are deferred until the complete session commits. Their failure reports an error after durable changes; it cannot undo a successful commit. Caller-owned connections remain caller-owned. - -`new DatabaseMigrationLock()` uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. Locks are session-owned, keyed by database/history table/scope, and remain held across migration commits. Do not switch databases, replace/close the connection or manipulate the native lock inside a migration. Unsupported providers, including SQLite, reject this lock implementation. Supply a custom `IMigrationLock` where another coordination mechanism is required. MySQL named locks coordinate one server, not an entire distributed cluster. - -## Planning and SQL preview - -`runner.Plan(target)` and `DryRun` inspect history without creating/upgrading it and do not invoke migration bodies, callbacks, transactions or SQLite PRAGMA changes. Custom providers must implement `IMigrationHistory` for these paths. - -`runner.PreviewSql(target, providerType)` connects for history/schema reads. `MigrationSqlPreview.Generate(providerType, migrations)` can generate SQL offline. Earlier structured operations update a planned schema so later operations can refer to newly created/renamed tables. SQL preview currently supports a subset: basic tables/columns, supported renames, simple indexes, inserts and raw SQL. Unsupported alterations, constraints, filters, callbacks and schema dependencies fail explicitly. Output is operation SQL, not an idempotent history-managed deployment bundle. - -Imperative bodies require `allowLegacyBodies: true`. Provider calls are captured through a rejecting proxy: direct connections, commands and unsupported reads/callbacks are blocked. **Arbitrary C# cannot be sandboxed**: constructors, fluent authoring and opted-in imperative bodies can still access files, networks or external state. Use trusted migration code. `InitializeOnce` and post-commit callbacks do not run during preview. - -## CLI from source - -```sh -dotnet pack src/Migrator.Tool -o artifacts/packages -dotnet tool install DotNetProjects.Migrator.Tool --add-source artifacts/packages --tool-path artifacts/tools -``` - -Set `MIGRATOR_CONNECTION` in your environment; the tool does not print its value. Common commands: - -```sh -migrator list --assembly MyMigrations.dll --provider SQLite -migrator status --assembly MyMigrations.dll --provider SQLite -migrator validate --assembly MyMigrations.dll --provider SQLite -migrator plan --assembly MyMigrations.dll --provider SQLite --target 10 -migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql -migrator sql --assembly MyMigrations.dll --provider SQLite --offline --output migration.sql -migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession -migrator rollback --assembly MyMigrations.dll --provider SQLite --target 0 -``` - -Use `--connection-env NAME`, `--schema`, `--tags a,b`, `--tag-match Any|All`, `--profiles a,b`, `--timeout SECONDS`, `--lock` and `--lock-timeout SECONDS` where applicable. `rollback` requires an explicit target. Offline SQL assumes empty history and currently rejects profiles/maintenance. `validate` validates version planning, not arbitrary migration-body behavior. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird. Other library providers need a custom host. - -Exit codes: `0` success, `1` execution/load failure, `2` invalid arguments, `3` unsupported operation/provider, `4` lock timeout. SQL output may contain migration data; exception and provider trace details are omitted from CLI diagnostics. - -## Optional DI and logging - -The source package `DotNetProjects.Migrator.Extensions.DependencyInjection` provides `services.AddMigrator(providerFactory, migrationAssembly, configureOptions)`. Resolve `Migrator` inside a service scope; migration constructors use that scope's services. Options are scoped snapshots. Provider disposal follows the DI scope. Microsoft logging records lifecycle events while omitting SQL text and raw exception messages; the core retains its lightweight logger API. - -## Validation - -Build before using the test scripts (they intentionally use `--no-build`): - -```sh -dotnet build Migrator.slnx -pwsh .github/scripts/test.ps1 -Database Unit -pwsh .github/scripts/test.ps1 -Database SQLite -``` - -See [live database tests](live-database-tests.md) for the full matrix. Provider-specific changes need live provider evidence. Check PR CI and review threads after every push; reply with implementation/test evidence and resolve fixed findings. Keep commits descriptive and merge the PR stack in dependency order only after review. +# Runner and fluent API upgrade + +These APIs describe the source upgrade under review in PRs #173, #174, #175 and #177. They are not a statement about the currently released NuGet packages. Build the repository to try them; no package publication is part of this change. + +## Fluent quick start + +The [compiled quick-start project](../examples/FluentQuickStart/Program.cs) executes preview, migration and automatic reversal against SQLite: + +```sh +dotnet run --project examples/FluentQuickStart +``` + +```csharp +[Migration(1, Scope = "demo"), Tags("core")] +public class CreateUsers : AutoReversingMigration +{ + public override void BuildUp(MigrationBuilder migration) + { + migration.Create.Table("Users") + .WithColumn("Id").AsInt32().PrimaryKey() + .WithColumn("Name").AsString(255).NotNullable(); + } +} +``` + +Use `DotNetProjects.Migrator`, `.Framework` and `.Framework.Fluent`. A table definition is completed before execution. Existing imperative `Migration.Up/Down` classes keep working. `FluentMigration` supports authored `BuildDown`; `AutoReversingMigration` reverses supported create/rename operations in reverse order. Destructive changes, data, SQL and callbacks need explicit reverse operations. Automatic reversal never restores deleted data. + +The builder has `Create`, `Alter`, `Delete`, `Rename`, `Insert`, `Update`, `Execute` and `Administration`. Schema inspection is exposed through `FluentMigration.Schema`, and the provider through `Context`. History and transaction methods remain explicit context operations. Administrative operations, views, data copying and updates from another table have typed operations; their SQL preview is currently unsupported. See the [operation coverage inventory](fluent-operation-coverage.md) for the normal API mappings and test limits. + +## Runner options + +`runner.Options` supports: + +| Option | Semantics | +| --- | --- | +| `Tags` / `TagMatch` | Ordinal names; explicit `Any` or `All`. No filter selects all versioned migrations. Filtered applied versions remain applied on downgrade. | +| `Profiles` | Explicit names of `[Profile("name")]` classes. Run after versioned migrations without recording versions; run again when selected again. | +| `TransactionMode` | `PerMigration` by default; `None` or `WholeSession` available. | +| `Activator` | Optional constructor activation delegate. | +| `Lock` / `LockTimeout` | Optional `IMigrationLock` lease; acquire before reading history and release on completion/failure. | + +Unscoped migrations inherit the provider scope; explicitly scoped migrations run only in that scope. Discovery, duplicate validation and history reads use the effective scope. Scopes separate history, not tables. Legacy custom providers can adopt the additive `IMigrationHistory` interface for read-only planning and effective-scope selection. + +Maintenance classes use `[Maintenance(MaintenanceStage.BeforeRun)]`, `BeforeMigration`, `AfterMigration` or `AfterRun`. Profiles and maintenance accept `Order` and `Scope`. Ordering uses `Order` then ordinal full type name. Hooks stop on failure; later hooks are not cleanup guarantees. Connection/transaction restoration and lock release do not depend on hooks running. Profiles and maintenance use `Up`; they do not acquire version records. + +## Transactions and locks + +`PerMigration` commits each successful migration. `None` leaves transaction behavior to the provider/operations. `WholeSession` is accepted for SQLite, PostgreSQL and SQL Server dialects; history-table initialization occurs before that transaction. Other dialects fail explicitly because transactional DDL has not been verified. Arbitrary imperative SQL can still violate transaction assumptions; database administration and implicit-commit statements require separate runs. + +`AfterUp`/`AfterDown` run after commit. In whole-session mode they are deferred until the complete session commits. Their failure reports an error after durable changes; it cannot undo a successful commit. Caller-owned connections remain caller-owned. + +`new DatabaseMigrationLock()` uses SQL Server application locks, PostgreSQL advisory locks or MySQL/MariaDB named locks. Locks are session-owned, keyed by database/history table/scope, and remain held across migration commits. Do not switch databases, replace/close the connection or manipulate the native lock inside a migration. Unsupported providers, including SQLite, reject this lock implementation. Supply a custom `IMigrationLock` where another coordination mechanism is required. MySQL named locks coordinate one server, not an entire distributed cluster. + +## Planning and SQL preview + +`runner.Plan(target)` and `DryRun` inspect history without creating/upgrading it and do not invoke migration bodies, callbacks, transactions or SQLite PRAGMA changes. Custom providers must implement `IMigrationHistory` for these paths. + +`runner.PreviewSql(target, providerType)` connects for history/schema reads. `MigrationSqlPreview.Generate(providerType, migrations)` can generate SQL offline. Earlier structured operations update a planned schema so later operations can refer to newly created/renamed tables. SQL preview currently supports a subset: basic tables/columns, supported renames, simple indexes, inserts and raw SQL. Unsupported alterations, constraints, filters, callbacks and schema dependencies fail explicitly. Output is operation SQL, not an idempotent history-managed deployment bundle. + +Imperative bodies require `allowLegacyBodies: true`. Provider calls are captured through a rejecting proxy: direct connections, commands and unsupported reads/callbacks are blocked. **Arbitrary C# cannot be sandboxed**: constructors, fluent authoring and opted-in imperative bodies can still access files, networks or external state. Use trusted migration code. Migrations overriding `InitializeOnce` are rejected before their body runs, because skipping initialization could produce misleading SQL. Post-commit callbacks do not run during preview. Raw SQL invalidates planned schema knowledge, so later structured schema dependencies fail explicitly. + +## CLI from source + +```sh +dotnet pack src/Migrator.Tool -o artifacts/packages +dotnet tool install DotNetProjects.Migrator.Tool --add-source artifacts/packages --tool-path artifacts/tools +``` + +Set `MIGRATOR_CONNECTION` in your environment; the tool does not print its value. Common commands: + +```sh +migrator list --assembly MyMigrations.dll --provider SQLite +migrator status --assembly MyMigrations.dll --provider SQLite +migrator validate --assembly MyMigrations.dll --provider SQLite +migrator plan --assembly MyMigrations.dll --provider SQLite --target 10 +migrator sql --assembly MyMigrations.dll --provider SQLite --output migration.sql +migrator sql --assembly MyMigrations.dll --provider SQLite --offline --output migration.sql +migrator migrate --assembly MyMigrations.dll --provider SQLite --scope billing --transaction WholeSession +migrator rollback --assembly MyMigrations.dll --provider SQLite --target 0 +``` + +Use `--connection-env NAME`, `--schema`, `--tags a,b`, `--tag-match Any|All`, `--profiles a,b`, `--timeout SECONDS`, `--lock` and `--lock-timeout SECONDS` where applicable. `rollback` requires an explicit lower target and rejects any plan containing upward steps. Target validation runs after acquiring the configured lock and refreshing history. Offline SQL assumes empty history and currently rejects profiles/maintenance. `validate` validates version planning, not arbitrary migration-body behavior. The packaged drivers cover SQLite, SQL Server, PostgreSQL, MySQL/MariaDB, Oracle and Firebird. Other library providers need a custom host. + +Exit codes: `0` success, `1` execution/load failure, `2` invalid arguments, `3` unsupported operation/provider, `4` lock timeout. SQL output may contain migration data; exception and provider trace details are omitted from CLI diagnostics. + +## Optional DI and logging + +The source package `DotNetProjects.Migrator.Extensions.DependencyInjection` provides `services.AddMigrator(providerFactory, migrationAssembly, configureOptions)`. Resolve `Migrator` inside a service scope; migration constructors use that scope's services. Options are scoped snapshots. Provider disposal follows the DI scope. Microsoft logging records lifecycle events while omitting SQL text and raw exception messages; the core retains its lightweight logger API. + +## Validation + +Build before using the test scripts (they intentionally use `--no-build`): + +```sh +dotnet build Migrator.slnx +pwsh .github/scripts/test.ps1 -Database Unit +pwsh .github/scripts/test.ps1 -Database SQLite +``` + +See [live database tests](live-database-tests.md) for the full matrix. Provider-specific changes need live provider evidence. Check PR CI and review threads after every push; reply with implementation/test evidence and resolve fixed findings. Keep commits descriptive and merge the PR stack in dependency order only after review. From 508218ab3f53199aa45a383c09ebc598e49896b2 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:10:30 +0200 Subject: [PATCH 13/17] Document script semantics, ownership cleanup and the complete issue inventory Record all 81 issue identities with verified closures, pending fixes and explicitly incomplete historical reproduction work. Update the source-pinned comparison and runner guide for SQL Server GO scripts, Oracle caller-owned legacy sequences and SQL Server uniqueness ownership. Record the observed Windows native-library path limitation. Validation: build, Unit 96 passed, SQLite 176 passed; compiled quick start passes. Locally packed tool 9.0.0-upgrade-review.1 generated offline SQL, then migrated/status-checked/rolled back SQLite from a short installation path. No package was published. --- docs/issue-audit.md | 99 ++++++++++++++++++++++++++ docs/migration-framework-comparison.md | 54 +++++++------- docs/runner-guide.md | 8 +++ 3 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 docs/issue-audit.md diff --git a/docs/issue-audit.md b/docs/issue-audit.md new file mode 100644 index 00000000..8116545b --- /dev/null +++ b/docs/issue-audit.md @@ -0,0 +1,99 @@ +# GitHub issue audit inventory + +Reviewed issue set: 81 issues (23 open and 58 closed at the start). Baseline: master `b7ae95c`; upgrade work is in PRs #173, #174, #175 and #177. Updated 2026-09-22. + +This inventory separates verified closures, fixes awaiting merge, partial fixes, and historical reports. A historical closed state is not proof of a fresh reproduction. The historical rows below identify relevant coverage but have **not all been independently reproduced**; keep that limitation visible until the per-issue audit is complete. No newly implemented fix is closed before its PR merges. + +Evidence used so far: clean master build and SQLite run (139 passed, one unrelated skipped default-removal test); master live matrix [35715528132](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35715528132); independent FK actions [35735648261](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35735648261); reproduced metadata/time failures [35737057890](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737057890). Later fixes require their own green checks. + +| Issue | Disposition | Reproduction / relevant evidence / remaining work | +| --- | --- | --- | +| [#15](https://github.com/dotnetprojects/Migrator.NET/issues/15) Feature to use update method for copying columns | Historically closed; retain state | CopyDataFromTableToTable / UpdateFromTableToTable fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#30](https://github.com/dotnetprojects/Migrator.NET/issues/30) Updates are not respecting command timeout | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#31](https://github.com/dotnetprojects/Migrator.NET/issues/31) Parameter names (and meaning) differ in ITransformationProvider and Implementation Class TransformationProvider | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#32](https://github.com/dotnetprojects/Migrator.NET/issues/32) Implementation of GetForeignKeyConstraints is wrong in TransformationProvider | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#33](https://github.com/dotnetprojects/Migrator.NET/issues/33) SQLite Foreign Keys: OnDelete, OnUpdate, Match is not implemented (ignored in SQLite) | Partial; keep open | SQLite independent DELETE/UPDATE actions now execute; MATCH semantics still need an explicit supported-policy decision. | +| [#34](https://github.com/dotnetprojects/Migrator.NET/issues/34) SQLite Foreign Keys: FKs added by AddTable are removed when using other methods | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#35](https://github.com/dotnetprojects/Migrator.NET/issues/35) SQLite: UNIQUEs are removed when using some other methods after AddTable | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#37](https://github.com/dotnetprojects/Migrator.NET/issues/37) Override in SQLite for AddForeignKey silently does nothing | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#38](https://github.com/dotnetprojects/Migrator.NET/issues/38) SQLite: Using AddTable with ColumnProperty.Unique silently does nothing | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#39](https://github.com/dotnetprojects/Migrator.NET/issues/39) SQLite: Indexes are dropped if certain methods are called which internally call changeColumnInternal | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#40](https://github.com/dotnetprojects/Migrator.NET/issues/40) Replace changeColumnInternal and implement different approach | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#41](https://github.com/dotnetprojects/Migrator.NET/issues/41) GetIndexes should distinguish between unique constraints and unique indexes | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#42](https://github.com/dotnetprojects/Migrator.NET/issues/42) Add GetUniques method for SQLiteTableInfo. This is utterly missing. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#43](https://github.com/dotnetprojects/Migrator.NET/issues/43) T | Historically closed; retain state | Report title is only “T”; no reproducible requirement in the retrieved issue body. No new fix or closure claimed. | +| [#44](https://github.com/dotnetprojects/Migrator.NET/issues/44) If ColumnProperty.PrimaryKey is removed, NotNull is removed as well | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#45](https://github.com/dotnetprojects/Migrator.NET/issues/45) ColumnProperty.ForeignKey has no own value but is combined using Unsigned and Null which is wrong | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#46](https://github.com/dotnetprojects/Migrator.NET/issues/46) SQLite: ConstraintExists returns false in any case (hard-coded). | Verified on master; closed | ConstraintExists reads SQLite metadata; clean master SQLite suite passed. | +| [#47](https://github.com/dotnetprojects/Migrator.NET/issues/47) SQLite: GetConstraints returns empty array in any case (hard-coded). | Verified on master; closed | GetConstraints no longer returns an unconditional empty array; generic constraint tests cover metadata. | +| [#48](https://github.com/dotnetprojects/Migrator.NET/issues/48) Schema is not supported in almost any case e.g. in AddTable | Partial; keep open | SQL Server schema-qualified column metadata corrected. Cross-provider schema qualification is not complete. | +| [#52](https://github.com/dotnetprojects/Migrator.NET/issues/52) AddForeignKey in TransformationProvider uses the same for OnUpdate and OnDelete which is wrong | Fixed in PR #174; await merge | Independent-action overload and provider guards; SQL Server update cascade/delete set-null regression passed live CI at b8b075e. | +| [#53](https://github.com/dotnetprojects/Migrator.NET/issues/53) QuoteColumnNames should return a new list instead of changing the given list | Fixed in PR #174; await merge | QuoteColumnNamesIfRequired returns a fresh array; FK inputs are copied. | +| [#54](https://github.com/dotnetprojects/Migrator.NET/issues/54) Constraint names are not quoted in many cases. Probably in all cases? | Partial; keep open | Generic removal and FK paths quote constraints. All provider-specific inline constraint paths still need review. | +| [#56](https://github.com/dotnetprojects/Migrator.NET/issues/56) public virtual bool ViewExists(string view) implementation is wrong | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#57](https://github.com/dotnetprojects/Migrator.NET/issues/57) public virtual bool TableExists(string view) implementation is wrong | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#59](https://github.com/dotnetprojects/Migrator.NET/issues/59) If NOT NULL or NULL is not explicitly given in the create script, notnull in PRAGMA table_info is wrong | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#60](https://github.com/dotnetprojects/Migrator.NET/issues/60) We cannot use NULL in columns of a composite PK | Verified on master; closed | SQLite composite Guid PK regression inserts NULL members and rejects non-null duplicates; single-column PK regression rejects NULL. | +| [#62](https://github.com/dotnetprojects/Migrator.NET/issues/62) PostgreSQL: '42883: function length(integer) does not exist | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#63](https://github.com/dotnetprojects/Migrator.NET/issues/63) T | Historically closed; retain state | Report title is only “T”; no reproducible requirement in the retrieved issue body. No new fix or closure claimed. | +| [#64](https://github.com/dotnetprojects/Migrator.NET/issues/64) CHECK Constraints are not implemented in SQLiteTransformationProvider | Verified on master; closed | SQLite CHECK support and valid/invalid data regressions exist. | +| [#65](https://github.com/dotnetprojects/Migrator.NET/issues/65) public override string[] GetConstraints(string table) returns an empty array in SQLite | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#66](https://github.com/dotnetprojects/Migrator.NET/issues/66) RemoveAllConstraints should be implemented in SQLite | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#68](https://github.com/dotnetprojects/Migrator.NET/issues/68) Match child properties and parent properties with data of PRAGMA foreign_key_list | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#72](https://github.com/dotnetprojects/Migrator.NET/issues/72) TableExistsShouldWorkWithBracketsAndSchemaNameAndTableName Test fails | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#73](https://github.com/dotnetprojects/Migrator.NET/issues/73) SqlServerDialect has incorrect boundaries defined for NVARCHAR(n). Should be 4000 | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#74](https://github.com/dotnetprojects/Migrator.NET/issues/74) Fix RemoveUnexistingColumn test for SQL Server | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#75](https://github.com/dotnetprojects/Migrator.NET/issues/75) Reactivate SQL Server Tests | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#82](https://github.com/dotnetprojects/Migrator.NET/issues/82) AddTable/AddForeignKey does not quote names - important for Postgre SQL | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#85](https://github.com/dotnetprojects/Migrator.NET/issues/85) Reactiveate MySQL tests | Verified on master; closed | PR #171 restored live MySQL/MariaDB tests; master CI run 35715528132 passed. | +| [#89](https://github.com/dotnetprojects/Migrator.NET/issues/89) GetColumns() in Postgre does not even read the type nor does it convert it to DBType! | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#90](https://github.com/dotnetprojects/Migrator.NET/issues/90) Default Values are not read correctly in Postgre using GetColumns() | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#92](https://github.com/dotnetprojects/Migrator.NET/issues/92) Add boolean default value tests for Postgre | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#95](https://github.com/dotnetprojects/Migrator.NET/issues/95) Postgre SQL interval default value is not implemented | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#97](https://github.com/dotnetprojects/Migrator.NET/issues/97) Postgres: GetColumnContentSize throws No function matches the given name and argument types. You might need to add explicit type casts. | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#98](https://github.com/dotnetprojects/Migrator.NET/issues/98) GetColumnContentSize should return int? instead of int for empty tables or NULL columns | Additive fix in PR #174; await merge | GetNullableColumnContentSize distinguishes empty/all-NULL input while keeping the existing int contract. | +| [#101](https://github.com/dotnetprojects/Migrator.NET/issues/101) GetColumns in SqlServerTransformationProvider swallows exceptions | Fixed in PR #174; await merge | SQL Server GetColumns propagates metadata errors rather than returning an empty schema. | +| [#102](https://github.com/dotnetprojects/Migrator.NET/issues/102) GetColumns_UniqueButNotPrimaryKey_ReturnsFalse should be moved to generic GetColumns tests | Reproduced; fix in PR #174 pending live CI | Moving the uniqueness test to generic fixtures reproduced missing UNIQUE flags on SQL Server, Oracle and PostgreSQL (run 35737057890). Added catalog queries and a composite-constraint counterexample. | +| [#103](https://github.com/dotnetprojects/Migrator.NET/issues/103) SQL Server: GetColumns parses datetime as DbType.Date instead of DbType.DateTime/DateTime2 - Major bug | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#104](https://github.com/dotnetprojects/Migrator.NET/issues/104) SQL Server: Default value of type DateTime/DateTime2 is not parsed | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#105](https://github.com/dotnetprojects/Migrator.NET/issues/105) Oracle: Only bool, Guid and DateTime are implemented in Default in OracleDialect | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#106](https://github.com/dotnetprojects/Migrator.NET/issues/106) SQL Server type detection should be completely overhauled - does not work correctly | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#107](https://github.com/dotnetprojects/Migrator.NET/issues/107) SQL Server parser of default values does not work correctly and implements only a few data types. Should be fixed and extended. | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#108](https://github.com/dotnetprojects/Migrator.NET/issues/108) Oracle: Dialect for byte array byte[] fails => OracleException (0x80004005): ORA-03062: Ein Komma oder eine rechte Klammer fehlen | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#109](https://github.com/dotnetprojects/Migrator.NET/issues/109) SQLite: RemoveForeignKey does nothing - silently! It is overridden but just returns - nothing else. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#110](https://github.com/dotnetprojects/Migrator.NET/issues/110) Implement GetCheckConstraints() - at least for generic tests | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#112](https://github.com/dotnetprojects/Migrator.NET/issues/112) No feedback if table or constraint does not exist in RemoveConstraint in TransformationProvider | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#113](https://github.com/dotnetprojects/Migrator.NET/issues/113) PrimaryKeyExists should be overridden and should throw in SQLite since it does not support named primary keys. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#114](https://github.com/dotnetprojects/Migrator.NET/issues/114) AddCheckConstraint is not overridden in SQLiteTransformationProvider | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#115](https://github.com/dotnetprojects/Migrator.NET/issues/115) Some AddColumn virtual methods are not overridden in SQLite resulting in cascading failure. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#118](https://github.com/dotnetprojects/Migrator.NET/issues/118) ColumnExists returns false in a catch! | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#120](https://github.com/dotnetprojects/Migrator.NET/issues/120) Extend Oracle restrictions from 30bytes to 128bytes supporting Oracle versions greater than 12.1 | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#122](https://github.com/dotnetprojects/Migrator.NET/issues/122) Oracle: AddIndex does not add a unique index if used in Index instance | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#123](https://github.com/dotnetprojects/Migrator.NET/issues/123) Indexes should be filterable | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#124](https://github.com/dotnetprojects/Migrator.NET/issues/124) PostgreSQL: AddIndex UNIQUE is not supported silently although available via Index class which is misleading | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#125](https://github.com/dotnetprojects/Migrator.NET/issues/125) Postgre: IncludeColumns in AddIndex is not used at all | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#126](https://github.com/dotnetprojects/Migrator.NET/issues/126) Postgre does neither extract included columns nor does it retrieve the partial filters | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#132](https://github.com/dotnetprojects/Migrator.NET/issues/132) SQL Server does not remove the unique index on ChangeColumn() with no ColumnProperty.Unique | Partial; keep open | New SQL Server column-owned uniqueness has an explicit extended-property marker. Historical unmarked objects are intentionally not inferred from names; need an ownership migration path. | +| [#134](https://github.com/dotnetprojects/Migrator.NET/issues/134) Remove hacks for some SQlite features | Partial; keep open | SQLite native rename/drop selected when eligible; guarded reconstruction retained. Unsupported table properties remain explicit failures. | +| [#135](https://github.com/dotnetprojects/Migrator.NET/issues/135) Feature CopyDataFromTableToTable | Historically closed; retain state | CopyDataFromTableToTable / UpdateFromTableToTable fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#139](https://github.com/dotnetprojects/Migrator.NET/issues/139) Default value is not reset on ChangeColumn | Fix in PR #174; await merge | Default removal regressions enabled; SQL Server default lookup and Oracle in-place reset corrected. | +| [#140](https://github.com/dotnetprojects/Migrator.NET/issues/140) Remove table in Oracle does not cleanup sequences | Fix in PR #174 pending live CI | Default Oracle RemoveTable no longer guesses sequence ownership. RemoveTableWithOwnedSequences validates explicit legacy names and propagates cleanup errors. | +| [#141](https://github.com/dotnetprojects/Migrator.NET/issues/141) RemoveTable in Oracle does not cleanup => TRIGGERs | Verification in PR #174 pending live CI | Oracle table-owned trigger cleanup is exercised by the legacy sequence/trigger regression; no guessed trigger-name cleanup. | +| [#143](https://github.com/dotnetprojects/Migrator.NET/issues/143) Replace Identity trigger to "GENERATED...." | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#145](https://github.com/dotnetprojects/Migrator.NET/issues/145) ExecuteScalar("SELECT MAX(Id) FROM MyTable") should return null (C#) if table is empty | Additive fix in PR #174; await merge | ExecuteNullableScalar returns null for null/DBNull and preserves typed structs; existing ExecuteScalar contract stays compatible. | +| [#146](https://github.com/dotnetprojects/Migrator.NET/issues/146) Oracle: Handle default value "NULL" | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#152](https://github.com/dotnetprojects/Migrator.NET/issues/152) Exception "This is currently not supported by the migrator see issue #44. You need to use NOT NULL for a PK column." occurs | Verified on master; closed | The old issue-44 exception is absent; SQLite supplies single-PK NOT NULL and supports nullable composite members in existing regressions. | +| [#161](https://github.com/dotnetprojects/Migrator.NET/issues/161) Microsoft SQLite: FK integrity issue when using AddTable (by e.g. using AddColumn) | Fix in PRs #173/#174; await merge | SQLite FK settings restored after success/failure; integrity checked before commit; rebuild dependencies guarded. Regression coverage uses both driver paths. | +| [#162](https://github.com/dotnetprojects/Migrator.NET/issues/162) DbType.Time is not implemented | Partial; keep open | SQL Server native TIME metadata/defaults and TimeSpan binding added after live reproduction. Oracle and SqlServer2005 retain documented historical representations; no universal native time claim. | +| [#164](https://github.com/dotnetprojects/Migrator.NET/issues/164) PostgreTransform Provider does not quote IncludeColumns for reserved names | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#165](https://github.com/dotnetprojects/Migrator.NET/issues/165) Included columns not quoted in Postgre | Duplicate verified; closed | Duplicates #164; PostgreSQL included-column quoting is covered by the live metadata regression. | +| [#167](https://github.com/dotnetprojects/Migrator.NET/issues/167) Get columns in postgre should not quote table name | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#169](https://github.com/dotnetprojects/Migrator.NET/issues/169) Support ON DELETE CASCADE (and other FK actions) in SQLite migrator | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | + +## Remaining audit work + +- Finish individual source/test evidence for historical closed reports; do not interpret a broad green suite as proof of every original report. +- Recheck partial schema qualification, inline identifier quoting, historical uniqueness ownership and provider-specific time representations. +- Verify the newest provider CI results before adding automatic closure references for #102, #140 and #141. +- Keep issue comments and this inventory synchronized as evidence changes. + diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index 1007de92..15013707 100644 --- a/docs/migration-framework-comparison.md +++ b/docs/migration-framework-comparison.md @@ -4,7 +4,7 @@ The main matrices cover **DotNetProjects.Migrator, FluentMigrator, EF Core migrations, DbUp and Evolve**—all five frameworks on the homepage. Additional sections cover **EF6, grate and RoundhousE**, with a short boundary comparison for **Flyway and Liquibase**. This is a defined shortlist, not a claim to catalogue every migration package ever published. -Migrator findings are pinned to upgrade-stack commit [`8d8818e`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. +Migrator findings are pinned to upgrade-stack commit [`39dc649`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. [Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Project README](../README.md) · [SQLite emulation comparison](#sqlite-emulation-comparison) · [Source index](#source-index) @@ -306,7 +306,7 @@ These interpretations are grounded in the preceding evidence, rather than univer Potential Migrator improvements, **not implemented-feature claims**: -1. Broader structured SQL-preview coverage, provider-specific batch scripts and CLI deployment validation. The source CLI and preview subset already exist. +1. Broader structured SQL-preview coverage, more client-script dialects and CLI deployment validation. SQL Server GO scripts now use an explicit batch path. The source CLI and preview subset already exist. 2. Validation of edits to already applied migration content. 3. More native lock backends and recovery/concurrency validation; three database families now have opt-in locks. 4. Repeatable migrations distinct from execution hooks. @@ -316,9 +316,9 @@ Potential Migrator improvements, **not implemented-feature claims**: ## Validation and maintenance -The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. At upgrade source `8d8818e`, a rebuilt solution passed **89 unit tests and 173 SQLite tests, with no skips**. The earlier tooling source `c0a7378` passed all eleven database/unit jobs and the coverage gate in [run 35733769486](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35733769486), including native lock tests on SQL Server, PostgreSQL, MySQL and MariaDB. Later changes require their own PR checks; a green earlier revision is not evidence for a later revision. +The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. At the earlier upgrade source `8d8818e`, a rebuilt solution passed **89 unit tests and 173 SQLite tests, with no skips**. The newly pinned revision passed **96 unit tests and 176 SQLite tests** locally. The packed/installed tool passed offline SQL, migration, status and rollback smoke checks. It must still be checked against its own live PR run. The earlier tooling source `c0a7378` passed all eleven database/unit jobs and the coverage gate in [run 35733769486](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35733769486), including native lock tests on SQL Server, PostgreSQL, MySQL and MariaDB. Later changes require their own PR checks; a green earlier revision is not evidence for a later revision. -This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; provider-specific batch scripts, several metadata/legacy ownership fixes and broader deployment regressions remain work in progress. The operation inventory maps normal API method families to fluent/context entry points, but does not establish every overload/provider combination through execution. Competitors were reviewed through documentation/source, **not executed in a comparative harness**. +This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; full client-script dialects, remaining metadata/legacy ownership cases and broader deployment regressions remain work in progress. SQL Server GO splitting and explicit Oracle legacy sequence cleanup are implemented. See the [81-issue inventory](issue-audit.md) for verified closures and incomplete audit items. The operation inventory maps normal API method families to fluent/context entry points, but does not establish every overload/provider combination through execution. Competitors were reviewed through documentation/source, **not executed in a comparative harness**. When updating: @@ -339,29 +339,29 @@ When updating: - **grate / RoundhousE:** [grate][g-home], [options][g-config], [script types][g-types], [migration guide][g-migrate], [RoundhousE][r-home]. - **SQLite engine:** [ALTER TABLE and reconstruction procedure][sqlite-alter]. -[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Migrator.cs -[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/MigrationLoader.cs -[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/MigrationExecution.cs -[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Framework/Migration.cs -[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Framework/ITransformationProvider.cs -[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Providers/TransformationProvider.cs -[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/ProviderFactory.cs -[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/docs/live-database-tests.md -[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs -[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs -[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs -[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs -[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs -[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs -[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs -[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs -[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs -[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs -[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs -[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs -[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs -[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/8d8818eba926cddbe8e30179f3bfab79ad03bde6/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs -[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/8d8818eba926cddbe8e30179f3bfab79ad03bde6/ +[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Migrator.cs +[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationLoader.cs +[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationExecution.cs +[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/Migration.cs +[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/ITransformationProvider.cs +[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/TransformationProvider.cs +[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/ProviderFactory.cs +[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/docs/live-database-tests.md +[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs +[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/39dc649626545785faa2fdd8df0affa6d16f1349/ [f-start]: https://fluentmigrator.github.io/intro/quick-start.html [f-config]: https://fluentmigrator.github.io/intro/configuration.html [f-sql]: https://fluentmigrator.github.io/operations/execute-sql.html diff --git a/docs/runner-guide.md b/docs/runner-guide.md index ccb5b544..5954eaaa 100644 --- a/docs/runner-guide.md +++ b/docs/runner-guide.md @@ -27,6 +27,12 @@ Use `DotNetProjects.Migrator`, `.Framework` and `.Framework.Fluent`. A table def The builder has `Create`, `Alter`, `Delete`, `Rename`, `Insert`, `Update`, `Execute` and `Administration`. Schema inspection is exposed through `FluentMigration.Schema`, and the provider through `Context`. History and transaction methods remain explicit context operations. Administrative operations, views, data copying and updates from another table have typed operations; their SQL preview is currently unsupported. See the [operation coverage inventory](fluent-operation-coverage.md) for the normal API mappings and test limits. +## Scripts and provider-specific cleanup + +`Execute.Script(path)` and `Execute.EmbeddedScript(assembly, resourceName)` capture script text as dedicated operations. Imperative callers can use `ExecuteScript(path)`, `ExecuteResourceScript(assembly, name)` and `ExecuteSqlScript(text)`. SQL Server splits standalone `GO` lines, including an optional `--` comment, while respecting strings, quoted identifiers and nested comments. GO repetition and SQLCMD directives fail explicitly before executing batches. Ordinary `ExecuteNonQuery` and fluent `Execute.Sql` never split client separators. Other providers receive the script as one command unless they implement `IScriptBatchProvider`; this is not a complete SQL*Plus, mysql-client or isql interpreter. + +Oracle `RemoveTable` leaves unrelated sequences intact and relies on Oracle to remove table-owned triggers and native identity objects. For legacy sequences you explicitly own, use `OracleTransformationProvider.RemoveTableWithOwnedSequences(table, sequenceNames)` through an explicit provider context/callback. It accepts simple unquoted sequence names, validates existence before dropping the table, and propagates cleanup failures. Oracle DDL is not atomic. SQL Server removes only column-unique constraints carrying its ownership marker; historical unmarked objects need an explicit migration rather than name guessing. + ## Runner options `runner.Options` supports: @@ -66,6 +72,8 @@ dotnet pack src/Migrator.Tool -o artifacts/packages dotnet tool install DotNetProjects.Migrator.Tool --add-source artifacts/packages --tool-path artifacts/tools ``` +On Windows, use a short tool installation directory (or the default global-tool directory): the bundled SQLite native library failed to load from this review workspace's deeply nested tool path, while the same package passed from a short temporary path. + Set `MIGRATOR_CONNECTION` in your environment; the tool does not print its value. Common commands: ```sh From 593ac153d0a1554953f76d9f04272023eaa9333c Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:39:25 +0200 Subject: [PATCH 14/17] Refresh comparison and ownership guidance against the integrated stack Pin the comparison to bb88165 and distinguish local validation from pending live checks. Document explicit SQL Server uniqueness adoption, auxiliary-only version preservation and callback context. Record verified Oracle cleanup and metadata CI evidence without closing partial issues. Validation: rebuilt solution; compiled FluentQuickStart preview, migration and reversal passed. Integrated runner validation: 93 unit and 184 SQLite tests passed. --- docs/issue-audit.md | 8 ++-- docs/migration-framework-comparison.md | 52 +++++++++++++------------- docs/runner-guide.md | 4 +- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/docs/issue-audit.md b/docs/issue-audit.md index 8116545b..bb3fe021 100644 --- a/docs/issue-audit.md +++ b/docs/issue-audit.md @@ -53,7 +53,7 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat | [#97](https://github.com/dotnetprojects/Migrator.NET/issues/97) Postgres: GetColumnContentSize throws No function matches the given name and argument types. You might need to add explicit type casts. | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | | [#98](https://github.com/dotnetprojects/Migrator.NET/issues/98) GetColumnContentSize should return int? instead of int for empty tables or NULL columns | Additive fix in PR #174; await merge | GetNullableColumnContentSize distinguishes empty/all-NULL input while keeping the existing int contract. | | [#101](https://github.com/dotnetprojects/Migrator.NET/issues/101) GetColumns in SqlServerTransformationProvider swallows exceptions | Fixed in PR #174; await merge | SQL Server GetColumns propagates metadata errors rather than returning an empty schema. | -| [#102](https://github.com/dotnetprojects/Migrator.NET/issues/102) GetColumns_UniqueButNotPrimaryKey_ReturnsFalse should be moved to generic GetColumns tests | Reproduced; fix in PR #174 pending live CI | Moving the uniqueness test to generic fixtures reproduced missing UNIQUE flags on SQL Server, Oracle and PostgreSQL (run 35737057890). Added catalog queries and a composite-constraint counterexample. | +| [#102](https://github.com/dotnetprojects/Migrator.NET/issues/102) GetColumns_UniqueButNotPrimaryKey_ReturnsFalse should be moved to generic GetColumns tests | Reproduced and verified in PR #174; await merge | Moving the uniqueness test to generic fixtures reproduced missing UNIQUE flags on SQL Server, Oracle and PostgreSQL (run 35737057890). Added catalog queries and a composite-constraint counterexample; all provider jobs passed run 35737814671. | | [#103](https://github.com/dotnetprojects/Migrator.NET/issues/103) SQL Server: GetColumns parses datetime as DbType.Date instead of DbType.DateTime/DateTime2 - Major bug | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | | [#104](https://github.com/dotnetprojects/Migrator.NET/issues/104) SQL Server: Default value of type DateTime/DateTime2 is not parsed | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | | [#105](https://github.com/dotnetprojects/Migrator.NET/issues/105) Oracle: Only bool, Guid and DateTime are implemented in Default in OracleDialect | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | @@ -73,12 +73,12 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat | [#124](https://github.com/dotnetprojects/Migrator.NET/issues/124) PostgreSQL: AddIndex UNIQUE is not supported silently although available via Index class which is misleading | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | | [#125](https://github.com/dotnetprojects/Migrator.NET/issues/125) Postgre: IncludeColumns in AddIndex is not used at all | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | | [#126](https://github.com/dotnetprojects/Migrator.NET/issues/126) Postgre does neither extract included columns nor does it retrieve the partial filters | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#132](https://github.com/dotnetprojects/Migrator.NET/issues/132) SQL Server does not remove the unique index on ChangeColumn() with no ColumnProperty.Unique | Partial; keep open | New SQL Server column-owned uniqueness has an explicit extended-property marker. Historical unmarked objects are intentionally not inferred from names; need an ownership migration path. | +| [#132](https://github.com/dotnetprojects/Migrator.NET/issues/132) SQL Server does not remove the unique index on ChangeColumn() with no ColumnProperty.Unique | Partial; keep open | New SQL Server column-owned uniqueness has an explicit extended-property marker. AdoptColumnUniqueConstraint now validates and marks an explicitly selected historical single-column UNIQUE constraint. Names never infer ownership; expanded live regressions await current CI. | | [#134](https://github.com/dotnetprojects/Migrator.NET/issues/134) Remove hacks for some SQlite features | Partial; keep open | SQLite native rename/drop selected when eligible; guarded reconstruction retained. Unsupported table properties remain explicit failures. | | [#135](https://github.com/dotnetprojects/Migrator.NET/issues/135) Feature CopyDataFromTableToTable | Historically closed; retain state | CopyDataFromTableToTable / UpdateFromTableToTable fixtures. Existing coverage identified; individual historical reproduction still pending. | | [#139](https://github.com/dotnetprojects/Migrator.NET/issues/139) Default value is not reset on ChangeColumn | Fix in PR #174; await merge | Default removal regressions enabled; SQL Server default lookup and Oracle in-place reset corrected. | -| [#140](https://github.com/dotnetprojects/Migrator.NET/issues/140) Remove table in Oracle does not cleanup sequences | Fix in PR #174 pending live CI | Default Oracle RemoveTable no longer guesses sequence ownership. RemoveTableWithOwnedSequences validates explicit legacy names and propagates cleanup errors. | -| [#141](https://github.com/dotnetprojects/Migrator.NET/issues/141) RemoveTable in Oracle does not cleanup => TRIGGERs | Verification in PR #174 pending live CI | Oracle table-owned trigger cleanup is exercised by the legacy sequence/trigger regression; no guessed trigger-name cleanup. | +| [#140](https://github.com/dotnetprojects/Migrator.NET/issues/140) Remove table in Oracle does not cleanup sequences | Verified fix in PR #174; await merge | Default Oracle RemoveTable no longer guesses sequence ownership. RemoveTableWithOwnedSequences validates explicit legacy names and propagates cleanup errors. Passed live Oracle CI in run 35737814671. | +| [#141](https://github.com/dotnetprojects/Migrator.NET/issues/141) RemoveTable in Oracle does not cleanup => TRIGGERs | Verified in PR #174; await merge | Oracle table-owned trigger cleanup is exercised by the legacy sequence/trigger regression; no guessed trigger-name cleanup. Passed live Oracle CI in run 35737814671. | | [#143](https://github.com/dotnetprojects/Migrator.NET/issues/143) Replace Identity trigger to "GENERATED...." | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | | [#145](https://github.com/dotnetprojects/Migrator.NET/issues/145) ExecuteScalar("SELECT MAX(Id) FROM MyTable") should return null (C#) if table is empty | Additive fix in PR #174; await merge | ExecuteNullableScalar returns null for null/DBNull and preserves typed structs; existing ExecuteScalar contract stays compatible. | | [#146](https://github.com/dotnetprojects/Migrator.NET/issues/146) Oracle: Handle default value "NULL" | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index 15013707..cc640917 100644 --- a/docs/migration-framework-comparison.md +++ b/docs/migration-framework-comparison.md @@ -4,7 +4,7 @@ The main matrices cover **DotNetProjects.Migrator, FluentMigrator, EF Core migrations, DbUp and Evolve**—all five frameworks on the homepage. Additional sections cover **EF6, grate and RoundhousE**, with a short boundary comparison for **Flyway and Liquibase**. This is a defined shortlist, not a claim to catalogue every migration package ever published. -Migrator findings are pinned to upgrade-stack commit [`39dc649`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. +Migrator findings are pinned to upgrade-stack commit [`bb88165`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. [Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Project README](../README.md) · [SQLite emulation comparison](#sqlite-emulation-comparison) · [Source index](#source-index) @@ -311,12 +311,12 @@ Potential Migrator improvements, **not implemented-feature claims**: 3. More native lock backends and recovery/concurrency validation; three database families now have opt-in locks. 4. Repeatable migrations distinct from execution hooks. 5. SQLite generated columns, table options, hidden rowid and complex-index preservation beyond the currently guarded subset. -6. Broader behavioral parity tests beyond the [fluent method-family inventory](fluent-operation-coverage.md), and safe migration of historical uniqueness objects without ownership markers. +6. Broader behavioral parity tests beyond the [fluent method-family inventory](fluent-operation-coverage.md), and provider coverage for explicit adoption of historical uniqueness objects without ownership markers. 7. Continued operation-level provider documentation and live test coverage. ## Validation and maintenance -The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. At the earlier upgrade source `8d8818e`, a rebuilt solution passed **89 unit tests and 173 SQLite tests, with no skips**. The newly pinned revision passed **96 unit tests and 176 SQLite tests** locally. The packed/installed tool passed offline SQL, migration, status and rollback smoke checks. It must still be checked against its own live PR run. The earlier tooling source `c0a7378` passed all eleven database/unit jobs and the coverage gate in [run 35733769486](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35733769486), including native lock tests on SQL Server, PostgreSQL, MySQL and MariaDB. Later changes require their own PR checks; a green earlier revision is not evidence for a later revision. +The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. The pinned upgrade revision passed **93 unit tests and 184 SQLite tests, with no skips**, after rebuilding the solution. Test counts reflect replacement of assertion-free tests with behavioral checks. The packed/installed tool previously passed offline SQL, migration, status and rollback smoke checks. The provider fixes at `bdc8ac3` passed all eleven database/unit jobs and the coverage gate in [run 35737814671](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737814671). The newly added concurrent-runner tests and later changes require their own PR checks; a green earlier revision is not evidence for a later revision. This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; full client-script dialects, remaining metadata/legacy ownership cases and broader deployment regressions remain work in progress. SQL Server GO splitting and explicit Oracle legacy sequence cleanup are implemented. See the [81-issue inventory](issue-audit.md) for verified closures and incomplete audit items. The operation inventory maps normal API method families to fluent/context entry points, but does not establish every overload/provider combination through execution. Competitors were reviewed through documentation/source, **not executed in a comparative harness**. @@ -339,29 +339,29 @@ When updating: - **grate / RoundhousE:** [grate][g-home], [options][g-config], [script types][g-types], [migration guide][g-migrate], [RoundhousE][r-home]. - **SQLite engine:** [ALTER TABLE and reconstruction procedure][sqlite-alter]. -[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Migrator.cs -[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationLoader.cs -[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationExecution.cs -[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/Migration.cs -[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/ITransformationProvider.cs -[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/TransformationProvider.cs -[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/ProviderFactory.cs -[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/docs/live-database-tests.md -[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs -[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs -[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs -[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs -[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs -[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs -[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs -[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs -[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs -[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs -[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs -[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs -[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs -[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/39dc649626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs -[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/39dc649626545785faa2fdd8df0affa6d16f1349/ +[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Migrator.cs +[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationLoader.cs +[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationExecution.cs +[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/Migration.cs +[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/ITransformationProvider.cs +[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/TransformationProvider.cs +[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/ProviderFactory.cs +[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/docs/live-database-tests.md +[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs +[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/bb88165626545785faa2fdd8df0affa6d16f1349/ [f-start]: https://fluentmigrator.github.io/intro/quick-start.html [f-config]: https://fluentmigrator.github.io/intro/configuration.html [f-sql]: https://fluentmigrator.github.io/operations/execute-sql.html diff --git a/docs/runner-guide.md b/docs/runner-guide.md index 5954eaaa..65066cbd 100644 --- a/docs/runner-guide.md +++ b/docs/runner-guide.md @@ -31,7 +31,7 @@ The builder has `Create`, `Alter`, `Delete`, `Rename`, `Insert`, `Update`, `Exec `Execute.Script(path)` and `Execute.EmbeddedScript(assembly, resourceName)` capture script text as dedicated operations. Imperative callers can use `ExecuteScript(path)`, `ExecuteResourceScript(assembly, name)` and `ExecuteSqlScript(text)`. SQL Server splits standalone `GO` lines, including an optional `--` comment, while respecting strings, quoted identifiers and nested comments. GO repetition and SQLCMD directives fail explicitly before executing batches. Ordinary `ExecuteNonQuery` and fluent `Execute.Sql` never split client separators. Other providers receive the script as one command unless they implement `IScriptBatchProvider`; this is not a complete SQL*Plus, mysql-client or isql interpreter. -Oracle `RemoveTable` leaves unrelated sequences intact and relies on Oracle to remove table-owned triggers and native identity objects. For legacy sequences you explicitly own, use `OracleTransformationProvider.RemoveTableWithOwnedSequences(table, sequenceNames)` through an explicit provider context/callback. It accepts simple unquoted sequence names, validates existence before dropping the table, and propagates cleanup failures. Oracle DDL is not atomic. SQL Server removes only column-unique constraints carrying its ownership marker; historical unmarked objects need an explicit migration rather than name guessing. +Oracle `RemoveTable` leaves unrelated sequences intact and relies on Oracle to remove table-owned triggers and native identity objects. For legacy sequences you explicitly own, use `OracleTransformationProvider.RemoveTableWithOwnedSequences(table, sequenceNames)` through an explicit provider context/callback. It accepts simple unquoted sequence names, validates existence before dropping the table, and propagates cleanup failures. Oracle DDL is not atomic. SQL Server removes only column-unique constraints carrying its ownership marker; historical unmarked objects can be adopted explicitly with `SqlServerTransformationProvider.AdoptColumnUniqueConstraint(table, column, constraint)`. Adoption verifies a single-column UNIQUE constraint before marking it and rejects composite constraints. Names alone never establish ownership. ## Runner options @@ -106,3 +106,5 @@ pwsh .github/scripts/test.ps1 -Database SQLite ``` See [live database tests](live-database-tests.md) for the full matrix. Provider-specific changes need live provider evidence. Check PR CI and review threads after every push; reply with implementation/test evidence and resolve fixed findings. Keep commits descriptive and merge the PR stack in dependency order only after review. + +An auxiliary-only `MigrateToLastVersion()` run preserves existing version history while executing selected profiles and maintenance. A completely empty run does not create a history table. Post-commit callbacks receive their migration context in both per-migration and whole-session modes; callback failure cannot undo a committed migration. From 5beaf051425c72020182fe7eee12703448e6e7c1 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:43:24 +0200 Subject: [PATCH 15/17] Record individual baseline evidence for the historical issue audit Map 43 historical reports to named passing NUnit cases from master run 35715528132. Record separate source evidence, behavior differences and remaining gaps for the other historical reports. Keep related coverage distinct from complete reproduction; no new issue closures are inferred from broad suite success. --- docs/issue-audit.md | 114 ++++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/docs/issue-audit.md b/docs/issue-audit.md index bb3fe021..4a93216a 100644 --- a/docs/issue-audit.md +++ b/docs/issue-audit.md @@ -2,93 +2,93 @@ Reviewed issue set: 81 issues (23 open and 58 closed at the start). Baseline: master `b7ae95c`; upgrade work is in PRs #173, #174, #175 and #177. Updated 2026-09-22. -This inventory separates verified closures, fixes awaiting merge, partial fixes, and historical reports. A historical closed state is not proof of a fresh reproduction. The historical rows below identify relevant coverage but have **not all been independently reproduced**; keep that limitation visible until the per-issue audit is complete. No newly implemented fix is closed before its PR merges. +This inventory separates verified closures, fixes awaiting merge, partial fixes, and historical reports. A historical closed state is not proof of a fresh reproduction. The historical rows below identify named passing baseline tests or explicit source evidence. They have **not all been independently reproduced from their original reports**; related coverage is labeled and must not be treated as complete behavioral proof. No newly implemented fix is closed before its PR merges. Evidence used so far: clean master build and SQLite run (139 passed, one unrelated skipped default-removal test); master live matrix [35715528132](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35715528132); independent FK actions [35735648261](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35735648261); reproduced metadata/time failures [35737057890](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737057890). Later fixes require their own green checks. | Issue | Disposition | Reproduction / relevant evidence / remaining work | | --- | --- | --- | -| [#15](https://github.com/dotnetprojects/Migrator.NET/issues/15) Feature to use update method for copying columns | Historically closed; retain state | CopyDataFromTableToTable / UpdateFromTableToTable fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#30](https://github.com/dotnetprojects/Migrator.NET/issues/30) Updates are not respecting command timeout | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | -| [#31](https://github.com/dotnetprojects/Migrator.NET/issues/31) Parameter names (and meaning) differ in ITransformationProvider and Implementation Class TransformationProvider | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | -| [#32](https://github.com/dotnetprojects/Migrator.NET/issues/32) Implementation of GetForeignKeyConstraints is wrong in TransformationProvider | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#15](https://github.com/dotnetprojects/Migrator.NET/issues/15) Feature to use update method for copying columns | Historically closed; relevant baseline test verified | `UpdateFromTableToTable_Success` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#30](https://github.com/dotnetprojects/Migrator.NET/issues/30) Updates are not respecting command timeout | Historical report rechecked; retain closed state | Master Update explicitly assigns CommandTimeout when configured and attaches the provider transaction before execution. No fresh wall-clock timeout reproduction was run; this is source evidence. | +| [#31](https://github.com/dotnetprojects/Migrator.NET/issues/31) Parameter names (and meaning) differ in ITransformationProvider and Implementation Class TransformationProvider | Historical report rechecked; retain closed state | The current interface names FK arguments childTable/childColumns and parentTable/parentColumns; PR #174 corrects the independent action path and definitions. Original report supplies screenshots only; no blanket claim for every parameter name. | +| [#32](https://github.com/dotnetprojects/Migrator.NET/issues/32) Implementation of GetForeignKeyConstraints is wrong in TransformationProvider | Historically closed; relevant baseline test verified | `GetForeignKeyConstraints_MultiColumnColumn_Success` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#33](https://github.com/dotnetprojects/Migrator.NET/issues/33) SQLite Foreign Keys: OnDelete, OnUpdate, Match is not implemented (ignored in SQLite) | Partial; keep open | SQLite independent DELETE/UPDATE actions now execute; MATCH semantics still need an explicit supported-policy decision. | -| [#34](https://github.com/dotnetprojects/Migrator.NET/issues/34) SQLite Foreign Keys: FKs added by AddTable are removed when using other methods | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#35](https://github.com/dotnetprojects/Migrator.NET/issues/35) SQLite: UNIQUEs are removed when using some other methods after AddTable | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#37](https://github.com/dotnetprojects/Migrator.NET/issues/37) Override in SQLite for AddForeignKey silently does nothing | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#38](https://github.com/dotnetprojects/Migrator.NET/issues/38) SQLite: Using AddTable with ColumnProperty.Unique silently does nothing | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#39](https://github.com/dotnetprojects/Migrator.NET/issues/39) SQLite: Indexes are dropped if certain methods are called which internally call changeColumnInternal | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#40](https://github.com/dotnetprojects/Migrator.NET/issues/40) Replace changeColumnInternal and implement different approach | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#41](https://github.com/dotnetprojects/Migrator.NET/issues/41) GetIndexes should distinguish between unique constraints and unique indexes | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#42](https://github.com/dotnetprojects/Migrator.NET/issues/42) Add GetUniques method for SQLiteTableInfo. This is utterly missing. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#34](https://github.com/dotnetprojects/Migrator.NET/issues/34) SQLite Foreign Keys: FKs added by AddTable are removed when using other methods | Historically closed; relevant baseline test verified | `AddForeignKey_RenameParentColumWithForeignKeyAndData_ForeignKeyPointsToRenamedColumn` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#35](https://github.com/dotnetprojects/Migrator.NET/issues/35) SQLite: UNIQUEs are removed when using some other methods after AddTable | Historically closed; relevant baseline test verified | `ChangeColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#37](https://github.com/dotnetprojects/Migrator.NET/issues/37) Override in SQLite for AddForeignKey silently does nothing | Historically closed; relevant baseline test verified | `AddForeignKey_Cascade_DeletingParentDeletesReferencingChildren` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#38](https://github.com/dotnetprojects/Migrator.NET/issues/38) SQLite: Using AddTable with ColumnProperty.Unique silently does nothing | Historically closed; relevant baseline test verified | `AddUniqueColumn` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#39](https://github.com/dotnetprojects/Migrator.NET/issues/39) SQLite: Indexes are dropped if certain methods are called which internally call changeColumnInternal | Historically closed; relevant baseline test verified | `AddColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#40](https://github.com/dotnetprojects/Migrator.NET/issues/40) Replace changeColumnInternal and implement different approach | Historically closed; relevant baseline test verified | `RecreateTable_HavingACompoundPrimaryKey_Success` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#41](https://github.com/dotnetprojects/Migrator.NET/issues/41) GetIndexes should distinguish between unique constraints and unique indexes | Historically closed; relevant baseline test verified | `GetSQLiteTableInfo_GetIndexesAndColumnsWithIndex_NoUniqueOnTheColumnsAndIndexExists` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#42](https://github.com/dotnetprojects/Migrator.NET/issues/42) Add GetUniques method for SQLiteTableInfo. This is utterly missing. | Historically closed; relevant baseline test verified | `GetUniques_Success` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#43](https://github.com/dotnetprojects/Migrator.NET/issues/43) T | Historically closed; retain state | Report title is only “T”; no reproducible requirement in the retrieved issue body. No new fix or closure claimed. | -| [#44](https://github.com/dotnetprojects/Migrator.NET/issues/44) If ColumnProperty.PrimaryKey is removed, NotNull is removed as well | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#45](https://github.com/dotnetprojects/Migrator.NET/issues/45) ColumnProperty.ForeignKey has no own value but is combined using Unsigned and Null which is wrong | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | +| [#44](https://github.com/dotnetprojects/Migrator.NET/issues/44) If ColumnProperty.PrimaryKey is removed, NotNull is removed as well | Historically closed; relevant baseline test verified | `AddPrimaryKey_AddPrimaryKey_ShouldStillBeNotNull` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#45](https://github.com/dotnetprojects/Migrator.NET/issues/45) ColumnProperty.ForeignKey has no own value but is combined using Unsigned and Null which is wrong | Historical report rechecked; retain closed state | Master ColumnProperty has no active ForeignKey enum member; the obsolete commented declaration is not a combined flag. The reported bit-mask implementation is absent. | | [#46](https://github.com/dotnetprojects/Migrator.NET/issues/46) SQLite: ConstraintExists returns false in any case (hard-coded). | Verified on master; closed | ConstraintExists reads SQLite metadata; clean master SQLite suite passed. | | [#47](https://github.com/dotnetprojects/Migrator.NET/issues/47) SQLite: GetConstraints returns empty array in any case (hard-coded). | Verified on master; closed | GetConstraints no longer returns an unconditional empty array; generic constraint tests cover metadata. | | [#48](https://github.com/dotnetprojects/Migrator.NET/issues/48) Schema is not supported in almost any case e.g. in AddTable | Partial; keep open | SQL Server schema-qualified column metadata corrected. Cross-provider schema qualification is not complete. | | [#52](https://github.com/dotnetprojects/Migrator.NET/issues/52) AddForeignKey in TransformationProvider uses the same for OnUpdate and OnDelete which is wrong | Fixed in PR #174; await merge | Independent-action overload and provider guards; SQL Server update cascade/delete set-null regression passed live CI at b8b075e. | | [#53](https://github.com/dotnetprojects/Migrator.NET/issues/53) QuoteColumnNames should return a new list instead of changing the given list | Fixed in PR #174; await merge | QuoteColumnNamesIfRequired returns a fresh array; FK inputs are copied. | | [#54](https://github.com/dotnetprojects/Migrator.NET/issues/54) Constraint names are not quoted in many cases. Probably in all cases? | Partial; keep open | Generic removal and FK paths quote constraints. All provider-specific inline constraint paths still need review. | -| [#56](https://github.com/dotnetprojects/Migrator.NET/issues/56) public virtual bool ViewExists(string view) implementation is wrong | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | -| [#57](https://github.com/dotnetprojects/Migrator.NET/issues/57) public virtual bool TableExists(string view) implementation is wrong | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | -| [#59](https://github.com/dotnetprojects/Migrator.NET/issues/59) If NOT NULL or NULL is not explicitly given in the create script, notnull in PRAGMA table_info is wrong | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#56](https://github.com/dotnetprojects/Migrator.NET/issues/56) public virtual bool ViewExists(string view) implementation is wrong | Historical report rechecked; retain closed state | Master live Oracle ViewExists_ViewExists_Returns and ViewExists_ViewDoesNotExist_ReturnsFalse both passed. Provider-specific overrides remain important; this does not certify arbitrary custom-provider implementations. | +| [#57](https://github.com/dotnetprojects/Migrator.NET/issues/57) public virtual bool TableExists(string view) implementation is wrong | Historical report rechecked; retain closed state | Master live SQL Server TableExists_WithSchemaNameTableExists_Returns and TableExists_TableDoesNotExist_ReturnsFalse passed. Qualified lookup gaps in other providers remain tracked in #48. | +| [#59](https://github.com/dotnetprojects/Migrator.NET/issues/59) If NOT NULL or NULL is not explicitly given in the create script, notnull in PRAGMA table_info is wrong | Historical report rechecked; retain closed state | Master SQLite AddTable_NoNotNullColumn_NotNullIsFalse and AddTable_NotNullColumn_NotNullIsTrue passed, distinguishing implicit nullable columns from explicit NOT NULL. | | [#60](https://github.com/dotnetprojects/Migrator.NET/issues/60) We cannot use NULL in columns of a composite PK | Verified on master; closed | SQLite composite Guid PK regression inserts NULL members and rejects non-null duplicates; single-column PK regression rejects NULL. | -| [#62](https://github.com/dotnetprojects/Migrator.NET/issues/62) PostgreSQL: '42883: function length(integer) does not exist | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#62](https://github.com/dotnetprojects/Migrator.NET/issues/62) PostgreSQL: '42883: function length(integer) does not exist | Historical report rechecked; retain closed state | Master PostgreSQL GetColumnContentSize_UseOnNonStringColumn_ThrowsSpeakingException passed. Non-string input is explicitly rejected rather than sent to length(integer). | | [#63](https://github.com/dotnetprojects/Migrator.NET/issues/63) T | Historically closed; retain state | Report title is only “T”; no reproducible requirement in the retrieved issue body. No new fix or closure claimed. | | [#64](https://github.com/dotnetprojects/Migrator.NET/issues/64) CHECK Constraints are not implemented in SQLiteTransformationProvider | Verified on master; closed | SQLite CHECK support and valid/invalid data regressions exist. | -| [#65](https://github.com/dotnetprojects/Migrator.NET/issues/65) public override string[] GetConstraints(string table) returns an empty array in SQLite | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#66](https://github.com/dotnetprojects/Migrator.NET/issues/66) RemoveAllConstraints should be implemented in SQLite | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#68](https://github.com/dotnetprojects/Migrator.NET/issues/68) Match child properties and parent properties with data of PRAGMA foreign_key_list | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#72](https://github.com/dotnetprojects/Migrator.NET/issues/72) TableExistsShouldWorkWithBracketsAndSchemaNameAndTableName Test fails | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#73](https://github.com/dotnetprojects/Migrator.NET/issues/73) SqlServerDialect has incorrect boundaries defined for NVARCHAR(n). Should be 4000 | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#74](https://github.com/dotnetprojects/Migrator.NET/issues/74) Fix RemoveUnexistingColumn test for SQL Server | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#75](https://github.com/dotnetprojects/Migrator.NET/issues/75) Reactivate SQL Server Tests | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#82](https://github.com/dotnetprojects/Migrator.NET/issues/82) AddTable/AddForeignKey does not quote names - important for Postgre SQL | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#65](https://github.com/dotnetprojects/Migrator.NET/issues/65) public override string[] GetConstraints(string table) returns an empty array in SQLite | Historically closed; relevant baseline test verified | `ConstraintExist` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#66](https://github.com/dotnetprojects/Migrator.NET/issues/66) RemoveAllConstraints should be implemented in SQLite | Historical report rechecked; retain closed state | Master RemoveAllConstraints cleared PK/UNIQUE but retained a CHECK TODO and foreign keys. PR #174 adds FK/CHECK removal; historical closure alone did not establish completeness. | +| [#68](https://github.com/dotnetprojects/Migrator.NET/issues/68) Match child properties and parent properties with data of PRAGMA foreign_key_list | Historically closed; relevant baseline test verified | `GetForeignKeyConstraints_SingleColumn_Success` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#72](https://github.com/dotnetprojects/Migrator.NET/issues/72) TableExistsShouldWorkWithBracketsAndSchemaNameAndTableName Test fails | Historically closed; relevant baseline test verified | `TableExistsShouldWorkWithBracketsAndSchemaNameAndTableName` passed in the SQLServer artifact of master run 35715528132 (2 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#73](https://github.com/dotnetprojects/Migrator.NET/issues/73) SqlServerDialect has incorrect boundaries defined for NVARCHAR(n). Should be 4000 | Historically closed; relevant baseline test verified | `AddTableWithFixedLengthEqualTo4000Characters_ShouldCreateNVARCHAR4000` passed in the SQLServer artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#74](https://github.com/dotnetprojects/Migrator.NET/issues/74) Fix RemoveUnexistingColumn test for SQL Server | Historically closed; relevant baseline test verified | `RemoveUnexistingColumn` passed in the SQLServer artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#75](https://github.com/dotnetprojects/Migrator.NET/issues/75) Reactivate SQL Server Tests | Historical report rechecked; retain closed state | Master workflow run 35715528132 contains a successful live SQL Server job and its NUnit artifact; the provider is enabled in the required CI matrix. | +| [#82](https://github.com/dotnetprojects/Migrator.NET/issues/82) AddTable/AddForeignKey does not quote names - important for Postgre SQL | Historical report rechecked; retain closed state | Master quotes reserved identifiers on several paths, with AddIndex_TableNameIsReservedWord_Succeeds passing. PR #174 expands constraint-name quoting. Full table/FK identifier coverage remains a limitation shared with #54. | | [#85](https://github.com/dotnetprojects/Migrator.NET/issues/85) Reactiveate MySQL tests | Verified on master; closed | PR #171 restored live MySQL/MariaDB tests; master CI run 35715528132 passed. | -| [#89](https://github.com/dotnetprojects/Migrator.NET/issues/89) GetColumns() in Postgre does not even read the type nor does it convert it to DBType! | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#90](https://github.com/dotnetprojects/Migrator.NET/issues/90) Default Values are not read correctly in Postgre using GetColumns() | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#92](https://github.com/dotnetprojects/Migrator.NET/issues/92) Add boolean default value tests for Postgre | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#95](https://github.com/dotnetprojects/Migrator.NET/issues/95) Postgre SQL interval default value is not implemented | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#97](https://github.com/dotnetprojects/Migrator.NET/issues/97) Postgres: GetColumnContentSize throws No function matches the given name and argument types. You might need to add explicit type casts. | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#89](https://github.com/dotnetprojects/Migrator.NET/issues/89) GetColumns() in Postgre does not even read the type nor does it convert it to DBType! | Historically closed; relevant baseline test verified | `GetColumns_DataTypeResolveSucceeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#90](https://github.com/dotnetprojects/Migrator.NET/issues/90) Default Values are not read correctly in Postgre using GetColumns() | Historically closed; relevant baseline test verified | `GetColumns_Postgres_DefaultValues_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#92](https://github.com/dotnetprojects/Migrator.NET/issues/92) Add boolean default value tests for Postgre | Historically closed; relevant baseline test verified | `GetColumns_DefaultValueBooleanValues_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (20 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#95](https://github.com/dotnetprojects/Migrator.NET/issues/95) Postgre SQL interval default value is not implemented | Historically closed; relevant baseline test verified | `GetColumns_Postgres_DefaultValues_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#97](https://github.com/dotnetprojects/Migrator.NET/issues/97) Postgres: GetColumnContentSize throws No function matches the given name and argument types. You might need to add explicit type casts. | Historically closed; relevant baseline test verified | `GetColumnContentSize_UseOnNonStringColumn_ThrowsSpeakingException` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#98](https://github.com/dotnetprojects/Migrator.NET/issues/98) GetColumnContentSize should return int? instead of int for empty tables or NULL columns | Additive fix in PR #174; await merge | GetNullableColumnContentSize distinguishes empty/all-NULL input while keeping the existing int contract. | | [#101](https://github.com/dotnetprojects/Migrator.NET/issues/101) GetColumns in SqlServerTransformationProvider swallows exceptions | Fixed in PR #174; await merge | SQL Server GetColumns propagates metadata errors rather than returning an empty schema. | | [#102](https://github.com/dotnetprojects/Migrator.NET/issues/102) GetColumns_UniqueButNotPrimaryKey_ReturnsFalse should be moved to generic GetColumns tests | Reproduced and verified in PR #174; await merge | Moving the uniqueness test to generic fixtures reproduced missing UNIQUE flags on SQL Server, Oracle and PostgreSQL (run 35737057890). Added catalog queries and a composite-constraint counterexample; all provider jobs passed run 35737814671. | -| [#103](https://github.com/dotnetprojects/Migrator.NET/issues/103) SQL Server: GetColumns parses datetime as DbType.Date instead of DbType.DateTime/DateTime2 - Major bug | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#104](https://github.com/dotnetprojects/Migrator.NET/issues/104) SQL Server: Default value of type DateTime/DateTime2 is not parsed | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#105](https://github.com/dotnetprojects/Migrator.NET/issues/105) Oracle: Only bool, Guid and DateTime are implemented in Default in OracleDialect | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#106](https://github.com/dotnetprojects/Migrator.NET/issues/106) SQL Server type detection should be completely overhauled - does not work correctly | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#107](https://github.com/dotnetprojects/Migrator.NET/issues/107) SQL Server parser of default values does not work correctly and implements only a few data types. Should be fixed and extended. | Historically closed; retain state | SQL Server metadata/default/type/schema fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#108](https://github.com/dotnetprojects/Migrator.NET/issues/108) Oracle: Dialect for byte array byte[] fails => OracleException (0x80004005): ORA-03062: Ein Komma oder eine rechte Klammer fehlen | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#109](https://github.com/dotnetprojects/Migrator.NET/issues/109) SQLite: RemoveForeignKey does nothing - silently! It is overridden but just returns - nothing else. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#110](https://github.com/dotnetprojects/Migrator.NET/issues/110) Implement GetCheckConstraints() - at least for generic tests | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | -| [#112](https://github.com/dotnetprojects/Migrator.NET/issues/112) No feedback if table or constraint does not exist in RemoveConstraint in TransformationProvider | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | -| [#113](https://github.com/dotnetprojects/Migrator.NET/issues/113) PrimaryKeyExists should be overridden and should throw in SQLite since it does not support named primary keys. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#114](https://github.com/dotnetprojects/Migrator.NET/issues/114) AddCheckConstraint is not overridden in SQLiteTransformationProvider | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#115](https://github.com/dotnetprojects/Migrator.NET/issues/115) Some AddColumn virtual methods are not overridden in SQLite resulting in cascading failure. | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#118](https://github.com/dotnetprojects/Migrator.NET/issues/118) ColumnExists returns false in a catch! | Historically closed; retain state | Generic provider, existence/constraint and command construction code. Existing coverage identified; individual historical reproduction still pending. | -| [#120](https://github.com/dotnetprojects/Migrator.NET/issues/120) Extend Oracle restrictions from 30bytes to 128bytes supporting Oracle versions greater than 12.1 | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#122](https://github.com/dotnetprojects/Migrator.NET/issues/122) Oracle: AddIndex does not add a unique index if used in Index instance | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | -| [#123](https://github.com/dotnetprojects/Migrator.NET/issues/123) Indexes should be filterable | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#124](https://github.com/dotnetprojects/Migrator.NET/issues/124) PostgreSQL: AddIndex UNIQUE is not supported silently although available via Index class which is misleading | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#125](https://github.com/dotnetprojects/Migrator.NET/issues/125) Postgre: IncludeColumns in AddIndex is not used at all | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#126](https://github.com/dotnetprojects/Migrator.NET/issues/126) Postgre does neither extract included columns nor does it retrieve the partial filters | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#103](https://github.com/dotnetprojects/Migrator.NET/issues/103) SQL Server: GetColumns parses datetime as DbType.Date instead of DbType.DateTime/DateTime2 - Major bug | Historically closed; relevant baseline test verified | `AddTableDateTime2` passed in the SQLServer artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#104](https://github.com/dotnetprojects/Migrator.NET/issues/104) SQL Server: Default value of type DateTime/DateTime2 is not parsed | Historically closed; relevant baseline test verified | `GetColumns_DefaultValues_Succeeds` passed in the SQLServer artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#105](https://github.com/dotnetprojects/Migrator.NET/issues/105) Oracle: Only bool, Guid and DateTime are implemented in Default in OracleDialect | Historically closed; relevant baseline test verified | `GetColumns_Oracle_DefaultValues_Succeeds` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#106](https://github.com/dotnetprojects/Migrator.NET/issues/106) SQL Server type detection should be completely overhauled - does not work correctly | Historically closed; relevant baseline test verified | `GetColumns_DefaultValues_Succeeds` passed in the SQLServer artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#107](https://github.com/dotnetprojects/Migrator.NET/issues/107) SQL Server parser of default values does not work correctly and implements only a few data types. Should be fixed and extended. | Historically closed; relevant baseline test verified | `GetColumns_DefaultValues_Succeeds` passed in the SQLServer artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#108](https://github.com/dotnetprojects/Migrator.NET/issues/108) Oracle: Dialect for byte array byte[] fails => OracleException (0x80004005): ORA-03062: Ein Komma oder eine rechte Klammer fehlen | Historically closed; relevant baseline test verified | `GetColumns_Oracle_DefaultValues_Succeeds` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#109](https://github.com/dotnetprojects/Migrator.NET/issues/109) SQLite: RemoveForeignKey does nothing - silently! It is overridden but just returns - nothing else. | Historically closed; relevant baseline test verified | `RemoveForeignKey` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#110](https://github.com/dotnetprojects/Migrator.NET/issues/110) Implement GetCheckConstraints() - at least for generic tests | Historically closed; relevant baseline test verified | `GetCheckConstraints_AddCheckConstraintsViaAddTable_CreatesTableCorrectly` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#112](https://github.com/dotnetprojects/Migrator.NET/issues/112) No feedback if table or constraint does not exist in RemoveConstraint in TransformationProvider | Historically closed; relevant baseline test verified | `RemoveUnexistingForeignKey` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#113](https://github.com/dotnetprojects/Migrator.NET/issues/113) PrimaryKeyExists should be overridden and should throw in SQLite since it does not support named primary keys. | Historical report rechecked; retain closed state | Master overrides PrimaryKeyExists and reports whether any primary key exists, deliberately ignoring the supplied name. This differs from the issue suggestion to throw; preserve compatibility and document the actual semantics. | +| [#114](https://github.com/dotnetprojects/Migrator.NET/issues/114) AddCheckConstraint is not overridden in SQLiteTransformationProvider | Historically closed; relevant baseline test verified | `CanAddCheckConstraint` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#115](https://github.com/dotnetprojects/Migrator.NET/issues/115) Some AddColumn virtual methods are not overridden in SQLite resulting in cascading failure. | Historically closed; relevant baseline test verified | `AddColumnWithDefaultButNoSize` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#118](https://github.com/dotnetprojects/Migrator.NET/issues/118) ColumnExists returns false in a catch! | Historical report rechecked; retain closed state | Master ColumnExists(table, column, ignoreCase) directly queries GetColumns without a catch. The exception-swallowing code in the report is absent. | +| [#120](https://github.com/dotnetprojects/Migrator.NET/issues/120) Extend Oracle restrictions from 30bytes to 128bytes supporting Oracle versions greater than 12.1 | Historical report rechecked; retain closed state | Master Oracle validation uses Encoding.UTF8.GetBytes(name).Length with a 128-byte limit. PR #174 repairs column-name validation to validate each actual column. Older Oracle versions have different limits. | +| [#122](https://github.com/dotnetprojects/Migrator.NET/issues/122) Oracle: AddIndex does not add a unique index if used in Index instance | Historically closed; relevant baseline test verified | `AddIndex_Unique_Success` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#123](https://github.com/dotnetprojects/Migrator.NET/issues/123) Indexes should be filterable | Historically closed; relevant baseline test verified | `AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#124](https://github.com/dotnetprojects/Migrator.NET/issues/124) PostgreSQL: AddIndex UNIQUE is not supported silently although available via Index class which is misleading | Historically closed; relevant baseline test verified | `AddIndex_Unique_Success` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#125](https://github.com/dotnetprojects/Migrator.NET/issues/125) Postgre: IncludeColumns in AddIndex is not used at all | Historically closed; relevant baseline test verified | `AddIndex_IncludeColumnsMultiple_Success` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#126](https://github.com/dotnetprojects/Migrator.NET/issues/126) Postgre does neither extract included columns nor does it retrieve the partial filters | Historically closed; relevant baseline test verified | `AddIndex_FilteredIndexSingle_Success` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#132](https://github.com/dotnetprojects/Migrator.NET/issues/132) SQL Server does not remove the unique index on ChangeColumn() with no ColumnProperty.Unique | Partial; keep open | New SQL Server column-owned uniqueness has an explicit extended-property marker. AdoptColumnUniqueConstraint now validates and marks an explicitly selected historical single-column UNIQUE constraint. Names never infer ownership; expanded live regressions await current CI. | | [#134](https://github.com/dotnetprojects/Migrator.NET/issues/134) Remove hacks for some SQlite features | Partial; keep open | SQLite native rename/drop selected when eligible; guarded reconstruction retained. Unsupported table properties remain explicit failures. | -| [#135](https://github.com/dotnetprojects/Migrator.NET/issues/135) Feature CopyDataFromTableToTable | Historically closed; retain state | CopyDataFromTableToTable / UpdateFromTableToTable fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#135](https://github.com/dotnetprojects/Migrator.NET/issues/135) Feature CopyDataFromTableToTable | Historically closed; relevant baseline test verified | `CopyDataFromTableToTable_UsingOrderBy_Success` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#139](https://github.com/dotnetprojects/Migrator.NET/issues/139) Default value is not reset on ChangeColumn | Fix in PR #174; await merge | Default removal regressions enabled; SQL Server default lookup and Oracle in-place reset corrected. | | [#140](https://github.com/dotnetprojects/Migrator.NET/issues/140) Remove table in Oracle does not cleanup sequences | Verified fix in PR #174; await merge | Default Oracle RemoveTable no longer guesses sequence ownership. RemoveTableWithOwnedSequences validates explicit legacy names and propagates cleanup errors. Passed live Oracle CI in run 35737814671. | | [#141](https://github.com/dotnetprojects/Migrator.NET/issues/141) RemoveTable in Oracle does not cleanup => TRIGGERs | Verified in PR #174; await merge | Oracle table-owned trigger cleanup is exercised by the legacy sequence/trigger regression; no guessed trigger-name cleanup. Passed live Oracle CI in run 35737814671. | -| [#143](https://github.com/dotnetprojects/Migrator.NET/issues/143) Replace Identity trigger to "GENERATED...." | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#143](https://github.com/dotnetprojects/Migrator.NET/issues/143) Replace Identity trigger to "GENERATED...." | Historically closed; relevant baseline test verified | `GetColumns_GetIdentity_Succeeds` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#145](https://github.com/dotnetprojects/Migrator.NET/issues/145) ExecuteScalar("SELECT MAX(Id) FROM MyTable") should return null (C#) if table is empty | Additive fix in PR #174; await merge | ExecuteNullableScalar returns null for null/DBNull and preserves typed structs; existing ExecuteScalar contract stays compatible. | -| [#146](https://github.com/dotnetprojects/Migrator.NET/issues/146) Oracle: Handle default value "NULL" | Historically closed; retain state | Oracle identity/default/index/identifier fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#146](https://github.com/dotnetprojects/Migrator.NET/issues/146) Oracle: Handle default value "NULL" | Historically closed; relevant baseline test verified | `DefaultValue_Null_Success` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#152](https://github.com/dotnetprojects/Migrator.NET/issues/152) Exception "This is currently not supported by the migrator see issue #44. You need to use NOT NULL for a PK column." occurs | Verified on master; closed | The old issue-44 exception is absent; SQLite supplies single-PK NOT NULL and supports nullable composite members in existing regressions. | | [#161](https://github.com/dotnetprojects/Migrator.NET/issues/161) Microsoft SQLite: FK integrity issue when using AddTable (by e.g. using AddColumn) | Fix in PRs #173/#174; await merge | SQLite FK settings restored after success/failure; integrity checked before commit; rebuild dependencies guarded. Regression coverage uses both driver paths. | | [#162](https://github.com/dotnetprojects/Migrator.NET/issues/162) DbType.Time is not implemented | Partial; keep open | SQL Server native TIME metadata/defaults and TimeSpan binding added after live reproduction. Oracle and SqlServer2005 retain documented historical representations; no universal native time claim. | -| [#164](https://github.com/dotnetprojects/Migrator.NET/issues/164) PostgreTransform Provider does not quote IncludeColumns for reserved names | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | +| [#164](https://github.com/dotnetprojects/Migrator.NET/issues/164) PostgreTransform Provider does not quote IncludeColumns for reserved names | Historically closed; relevant baseline test verified | `AddIndex_IncludeColumnsWithReservedWord_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#165](https://github.com/dotnetprojects/Migrator.NET/issues/165) Included columns not quoted in Postgre | Duplicate verified; closed | Duplicates #164; PostgreSQL included-column quoting is covered by the live metadata regression. | -| [#167](https://github.com/dotnetprojects/Migrator.NET/issues/167) Get columns in postgre should not quote table name | Historically closed; retain state | PostgreSQL metadata/index/default/content-size fixtures and live metadata regressions. Existing coverage identified; individual historical reproduction still pending. | -| [#169](https://github.com/dotnetprojects/Migrator.NET/issues/169) Support ON DELETE CASCADE (and other FK actions) in SQLite migrator | Historically closed; retain state | SQLite provider reconstruction, constraint and metadata fixtures. Existing coverage identified; individual historical reproduction still pending. | +| [#167](https://github.com/dotnetprojects/Migrator.NET/issues/167) Get columns in postgre should not quote table name | Historically closed; relevant baseline test verified | `AddIndex_TableNameIsReservedWord_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | +| [#169](https://github.com/dotnetprojects/Migrator.NET/issues/169) Support ON DELETE CASCADE (and other FK actions) in SQLite migrator | Historically closed; relevant baseline test verified | `AddForeignKey_Cascade_DeletingParentDeletesReferencingChildren` passed in the SQLite artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | ## Remaining audit work From b254927847b600865bc1d774eb765dd4deb620b1 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 16:51:32 +0200 Subject: [PATCH 16/17] Document PostgreSQL metadata scope and verified concurrent-runner coverage Refresh the pinned source revision, record native lock concurrency evidence for four engines and explain the new PostgreSQL relation lookup and time mapping. Keep broader schema support and original-report reproduction limits explicit. Validation: integrated solution build, 93 unit tests and 184 SQLite tests passed. New PostgreSQL regressions await live PR CI. --- docs/issue-audit.md | 8 ++--- docs/migration-framework-comparison.md | 50 +++++++++++++------------- docs/runner-guide.md | 2 ++ 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/issue-audit.md b/docs/issue-audit.md index 4a93216a..2fd6b6b5 100644 --- a/docs/issue-audit.md +++ b/docs/issue-audit.md @@ -26,7 +26,7 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat | [#45](https://github.com/dotnetprojects/Migrator.NET/issues/45) ColumnProperty.ForeignKey has no own value but is combined using Unsigned and Null which is wrong | Historical report rechecked; retain closed state | Master ColumnProperty has no active ForeignKey enum member; the obsolete commented declaration is not a combined flag. The reported bit-mask implementation is absent. | | [#46](https://github.com/dotnetprojects/Migrator.NET/issues/46) SQLite: ConstraintExists returns false in any case (hard-coded). | Verified on master; closed | ConstraintExists reads SQLite metadata; clean master SQLite suite passed. | | [#47](https://github.com/dotnetprojects/Migrator.NET/issues/47) SQLite: GetConstraints returns empty array in any case (hard-coded). | Verified on master; closed | GetConstraints no longer returns an unconditional empty array; generic constraint tests cover metadata. | -| [#48](https://github.com/dotnetprojects/Migrator.NET/issues/48) Schema is not supported in almost any case e.g. in AddTable | Partial; keep open | SQL Server schema-qualified column metadata corrected. Cross-provider schema qualification is not complete. | +| [#48](https://github.com/dotnetprojects/Migrator.NET/issues/48) Schema is not supported in almost any case e.g. in AddTable | Partial; keep open | SQL Server schema-qualified column metadata corrected. PostgreSQL relation-based, parameterized column/constraint/existence lookup now has cross-schema and quoted-name regressions in PR #174; current CI pending. Cross-provider schema qualification is not complete. | | [#52](https://github.com/dotnetprojects/Migrator.NET/issues/52) AddForeignKey in TransformationProvider uses the same for OnUpdate and OnDelete which is wrong | Fixed in PR #174; await merge | Independent-action overload and provider guards; SQL Server update cascade/delete set-null regression passed live CI at b8b075e. | | [#53](https://github.com/dotnetprojects/Migrator.NET/issues/53) QuoteColumnNames should return a new list instead of changing the given list | Fixed in PR #174; await merge | QuoteColumnNamesIfRequired returns a fresh array; FK inputs are copied. | | [#54](https://github.com/dotnetprojects/Migrator.NET/issues/54) Constraint names are not quoted in many cases. Probably in all cases? | Partial; keep open | Generic removal and FK paths quote constraints. All provider-specific inline constraint paths still need review. | @@ -73,7 +73,7 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat | [#124](https://github.com/dotnetprojects/Migrator.NET/issues/124) PostgreSQL: AddIndex UNIQUE is not supported silently although available via Index class which is misleading | Historically closed; relevant baseline test verified | `AddIndex_Unique_Success` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#125](https://github.com/dotnetprojects/Migrator.NET/issues/125) Postgre: IncludeColumns in AddIndex is not used at all | Historically closed; relevant baseline test verified | `AddIndex_IncludeColumnsMultiple_Success` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#126](https://github.com/dotnetprojects/Migrator.NET/issues/126) Postgre does neither extract included columns nor does it retrieve the partial filters | Historically closed; relevant baseline test verified | `AddIndex_FilteredIndexSingle_Success` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | -| [#132](https://github.com/dotnetprojects/Migrator.NET/issues/132) SQL Server does not remove the unique index on ChangeColumn() with no ColumnProperty.Unique | Partial; keep open | New SQL Server column-owned uniqueness has an explicit extended-property marker. AdoptColumnUniqueConstraint now validates and marks an explicitly selected historical single-column UNIQUE constraint. Names never infer ownership; expanded live regressions await current CI. | +| [#132](https://github.com/dotnetprojects/Migrator.NET/issues/132) SQL Server does not remove the unique index on ChangeColumn() with no ColumnProperty.Unique | Partial; keep open | New SQL Server column-owned uniqueness has an explicit extended-property marker. AdoptColumnUniqueConstraint now validates and marks an explicitly selected historical single-column UNIQUE constraint. Names never infer ownership; adoption and composite-rejection regressions passed live SQL Server in run 35741656276. | | [#134](https://github.com/dotnetprojects/Migrator.NET/issues/134) Remove hacks for some SQlite features | Partial; keep open | SQLite native rename/drop selected when eligible; guarded reconstruction retained. Unsupported table properties remain explicit failures. | | [#135](https://github.com/dotnetprojects/Migrator.NET/issues/135) Feature CopyDataFromTableToTable | Historically closed; relevant baseline test verified | `CopyDataFromTableToTable_UsingOrderBy_Success` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#139](https://github.com/dotnetprojects/Migrator.NET/issues/139) Default value is not reset on ChangeColumn | Fix in PR #174; await merge | Default removal regressions enabled; SQL Server default lookup and Oracle in-place reset corrected. | @@ -84,7 +84,7 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat | [#146](https://github.com/dotnetprojects/Migrator.NET/issues/146) Oracle: Handle default value "NULL" | Historically closed; relevant baseline test verified | `DefaultValue_Null_Success` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#152](https://github.com/dotnetprojects/Migrator.NET/issues/152) Exception "This is currently not supported by the migrator see issue #44. You need to use NOT NULL for a PK column." occurs | Verified on master; closed | The old issue-44 exception is absent; SQLite supplies single-PK NOT NULL and supports nullable composite members in existing regressions. | | [#161](https://github.com/dotnetprojects/Migrator.NET/issues/161) Microsoft SQLite: FK integrity issue when using AddTable (by e.g. using AddColumn) | Fix in PRs #173/#174; await merge | SQLite FK settings restored after success/failure; integrity checked before commit; rebuild dependencies guarded. Regression coverage uses both driver paths. | -| [#162](https://github.com/dotnetprojects/Migrator.NET/issues/162) DbType.Time is not implemented | Partial; keep open | SQL Server native TIME metadata/defaults and TimeSpan binding added after live reproduction. Oracle and SqlServer2005 retain documented historical representations; no universal native time claim. | +| [#162](https://github.com/dotnetprojects/Migrator.NET/issues/162) DbType.Time is not implemented | Partial; keep open | SQL Server native TIME metadata/defaults and TimeSpan binding verified live. PostgreSQL native TIME metadata/default parsing now has a regression in PR #174; current CI pending. Oracle and SqlServer2005 retain documented historical representations; no universal native time claim. | | [#164](https://github.com/dotnetprojects/Migrator.NET/issues/164) PostgreTransform Provider does not quote IncludeColumns for reserved names | Historically closed; relevant baseline test verified | `AddIndex_IncludeColumnsWithReservedWord_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#165](https://github.com/dotnetprojects/Migrator.NET/issues/165) Included columns not quoted in Postgre | Duplicate verified; closed | Duplicates #164; PostgreSQL included-column quoting is covered by the live metadata regression. | | [#167](https://github.com/dotnetprojects/Migrator.NET/issues/167) Get columns in postgre should not quote table name | Historically closed; relevant baseline test verified | `AddIndex_TableNameIsReservedWord_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | @@ -94,6 +94,6 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat - Finish individual source/test evidence for historical closed reports; do not interpret a broad green suite as proof of every original report. - Recheck partial schema qualification, inline identifier quoting, historical uniqueness ownership and provider-specific time representations. -- Verify the newest provider CI results before adding automatic closure references for #102, #140 and #141. +- Keep partial ownership/scope reports open; new complete fixes close only after the referenced PR merges. - Keep issue comments and this inventory synchronized as evidence changes. diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index cc640917..af015450 100644 --- a/docs/migration-framework-comparison.md +++ b/docs/migration-framework-comparison.md @@ -4,7 +4,7 @@ The main matrices cover **DotNetProjects.Migrator, FluentMigrator, EF Core migrations, DbUp and Evolve**—all five frameworks on the homepage. Additional sections cover **EF6, grate and RoundhousE**, with a short boundary comparison for **Flyway and Liquibase**. This is a defined shortlist, not a claim to catalogue every migration package ever published. -Migrator findings are pinned to upgrade-stack commit [`bb88165`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. +Migrator findings are pinned to upgrade-stack commit [`bc35e0e`][m-revision]. These are source capabilities under review in PRs [#173](https://github.com/dotnetprojects/Migrator.NET/pull/173), [#174](https://github.com/dotnetprojects/Migrator.NET/pull/174), [#175](https://github.com/dotnetprojects/Migrator.NET/pull/175) and [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), **not a claim that these features have shipped on NuGet**. FluentMigrator's SQLite implementation is pinned to [`2e0acdb`][f-sqlite-generator]. Other findings describe the linked official documentation as reviewed, not guaranteed behavior of every historical release. EF Core features introduced in version 9 are labeled. Check provider and release compatibility separately. [Homepage](https://dotnetprojects.github.io/Migrator.NET/) · [Project README](../README.md) · [SQLite emulation comparison](#sqlite-emulation-comparison) · [Source index](#source-index) @@ -316,7 +316,7 @@ Potential Migrator improvements, **not implemented-feature claims**: ## Validation and maintenance -The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. The pinned upgrade revision passed **93 unit tests and 184 SQLite tests, with no skips**, after rebuilding the solution. Test counts reflect replacement of assertion-free tests with behavioral checks. The packed/installed tool previously passed offline SQL, migration, status and rollback smoke checks. The provider fixes at `bdc8ac3` passed all eleven database/unit jobs and the coverage gate in [run 35737814671](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737814671). The newly added concurrent-runner tests and later changes require their own PR checks; a green earlier revision is not evidence for a later revision. +The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. The pinned upgrade revision passed **93 unit tests and 184 SQLite tests, with no skips**, after rebuilding the solution. Test counts reflect replacement of assertion-free tests with behavioral checks. The packed/installed tool previously passed offline SQL, migration, status and rollback smoke checks. The provider fixes at `bdc8ac3` passed all eleven database/unit jobs and the coverage gate in [run 35737814671](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737814671). Concurrent-runner tests passed on SQL Server, PostgreSQL, MySQL and MariaDB at `bb88165` in [run 35741656276](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35741656276). The later PostgreSQL metadata/time changes require their own PR checks; a green earlier revision is not evidence for a later revision. This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; full client-script dialects, remaining metadata/legacy ownership cases and broader deployment regressions remain work in progress. SQL Server GO splitting and explicit Oracle legacy sequence cleanup are implemented. See the [81-issue inventory](issue-audit.md) for verified closures and incomplete audit items. The operation inventory maps normal API method families to fluent/context entry points, but does not establish every overload/provider combination through execution. Competitors were reviewed through documentation/source, **not executed in a comparative harness**. @@ -339,29 +339,29 @@ When updating: - **grate / RoundhousE:** [grate][g-home], [options][g-config], [script types][g-types], [migration guide][g-migrate], [RoundhousE][r-home]. - **SQLite engine:** [ALTER TABLE and reconstruction procedure][sqlite-alter]. -[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Migrator.cs -[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationLoader.cs -[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationExecution.cs -[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/Migration.cs -[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/ITransformationProvider.cs -[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/TransformationProvider.cs -[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/ProviderFactory.cs -[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/docs/live-database-tests.md -[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs -[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs -[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs -[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs -[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs -[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs -[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs -[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs -[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs -[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs -[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs -[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs -[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs -[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/bb88165626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs -[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/bb88165626545785faa2fdd8df0affa6d16f1349/ +[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/Migrator.cs +[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationLoader.cs +[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/MigrationExecution.cs +[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/Migration.cs +[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/Framework/ITransformationProvider.cs +[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/TransformationProvider.cs +[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/ProviderFactory.cs +[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/docs/live-database-tests.md +[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs +[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/bc35e0e626545785faa2fdd8df0affa6d16f1349/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/bc35e0e626545785faa2fdd8df0affa6d16f1349/ [f-start]: https://fluentmigrator.github.io/intro/quick-start.html [f-config]: https://fluentmigrator.github.io/intro/configuration.html [f-sql]: https://fluentmigrator.github.io/operations/execute-sql.html diff --git a/docs/runner-guide.md b/docs/runner-guide.md index 65066cbd..8689ef43 100644 --- a/docs/runner-guide.md +++ b/docs/runner-guide.md @@ -108,3 +108,5 @@ pwsh .github/scripts/test.ps1 -Database SQLite See [live database tests](live-database-tests.md) for the full matrix. Provider-specific changes need live provider evidence. Check PR CI and review threads after every push; reply with implementation/test evidence and resolve fixed findings. Keep commits descriptive and merge the PR stack in dependency order only after review. An auxiliary-only `MigrateToLastVersion()` run preserves existing version history while executing selected profiles and maintenance. A completely empty run does not create a history table. Post-commit callbacks receive their migration context in both per-migration and whole-session modes; callback failure cannot undo a committed migration. + +PostgreSQL column and constraint metadata resolves the requested relation through the database, including schema-qualified or explicitly quoted names and the connection search path. The lookup is parameterized and distinguishes same-named tables in different schemas. This does not imply complete schema qualification for every provider operation. Native `time without time zone` metadata and literal defaults map to `TimeSpan`. From 8ee8d39df5b3cc872757170ae745b7daf8e4d632 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 17:01:05 +0200 Subject: [PATCH 17/17] Record successful CI for the final integrated source revision Replace pending PostgreSQL validation notes with the actual live regression result. Link the complete database/unit matrix and coverage gate for bc35e0e while retaining the distinction between verified behavior and incomplete audit scenarios. Documentation-only evidence update. --- docs/issue-audit.md | 6 +++--- docs/migration-framework-comparison.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/issue-audit.md b/docs/issue-audit.md index 2fd6b6b5..477cfd4b 100644 --- a/docs/issue-audit.md +++ b/docs/issue-audit.md @@ -4,7 +4,7 @@ Reviewed issue set: 81 issues (23 open and 58 closed at the start). Baseline: ma This inventory separates verified closures, fixes awaiting merge, partial fixes, and historical reports. A historical closed state is not proof of a fresh reproduction. The historical rows below identify named passing baseline tests or explicit source evidence. They have **not all been independently reproduced from their original reports**; related coverage is labeled and must not be treated as complete behavioral proof. No newly implemented fix is closed before its PR merges. -Evidence used so far: clean master build and SQLite run (139 passed, one unrelated skipped default-removal test); master live matrix [35715528132](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35715528132); independent FK actions [35735648261](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35735648261); reproduced metadata/time failures [35737057890](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737057890). Later fixes require their own green checks. +Evidence used so far: clean master build and SQLite run (139 passed, one unrelated skipped default-removal test); master live matrix [35715528132](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35715528132); independent FK actions [35735648261](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35735648261); reproduced metadata/time failures [35737057890](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737057890). The integrated source `bc35e0e` passed all eleven database/unit jobs and the coverage gate in [run 35743265022](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35743265022). Later fixes require their own green checks. | Issue | Disposition | Reproduction / relevant evidence / remaining work | | --- | --- | --- | @@ -26,7 +26,7 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat | [#45](https://github.com/dotnetprojects/Migrator.NET/issues/45) ColumnProperty.ForeignKey has no own value but is combined using Unsigned and Null which is wrong | Historical report rechecked; retain closed state | Master ColumnProperty has no active ForeignKey enum member; the obsolete commented declaration is not a combined flag. The reported bit-mask implementation is absent. | | [#46](https://github.com/dotnetprojects/Migrator.NET/issues/46) SQLite: ConstraintExists returns false in any case (hard-coded). | Verified on master; closed | ConstraintExists reads SQLite metadata; clean master SQLite suite passed. | | [#47](https://github.com/dotnetprojects/Migrator.NET/issues/47) SQLite: GetConstraints returns empty array in any case (hard-coded). | Verified on master; closed | GetConstraints no longer returns an unconditional empty array; generic constraint tests cover metadata. | -| [#48](https://github.com/dotnetprojects/Migrator.NET/issues/48) Schema is not supported in almost any case e.g. in AddTable | Partial; keep open | SQL Server schema-qualified column metadata corrected. PostgreSQL relation-based, parameterized column/constraint/existence lookup now has cross-schema and quoted-name regressions in PR #174; current CI pending. Cross-provider schema qualification is not complete. | +| [#48](https://github.com/dotnetprojects/Migrator.NET/issues/48) Schema is not supported in almost any case e.g. in AddTable | Partial; keep open | SQL Server schema-qualified column metadata corrected. PostgreSQL relation-based, parameterized column/constraint/existence lookup now has cross-schema and quoted-name regressions in PR #174; passed live PostgreSQL CI in run 35742976746. Cross-provider schema qualification is not complete. | | [#52](https://github.com/dotnetprojects/Migrator.NET/issues/52) AddForeignKey in TransformationProvider uses the same for OnUpdate and OnDelete which is wrong | Fixed in PR #174; await merge | Independent-action overload and provider guards; SQL Server update cascade/delete set-null regression passed live CI at b8b075e. | | [#53](https://github.com/dotnetprojects/Migrator.NET/issues/53) QuoteColumnNames should return a new list instead of changing the given list | Fixed in PR #174; await merge | QuoteColumnNamesIfRequired returns a fresh array; FK inputs are copied. | | [#54](https://github.com/dotnetprojects/Migrator.NET/issues/54) Constraint names are not quoted in many cases. Probably in all cases? | Partial; keep open | Generic removal and FK paths quote constraints. All provider-specific inline constraint paths still need review. | @@ -84,7 +84,7 @@ Evidence used so far: clean master build and SQLite run (139 passed, one unrelat | [#146](https://github.com/dotnetprojects/Migrator.NET/issues/146) Oracle: Handle default value "NULL" | Historically closed; relevant baseline test verified | `DefaultValue_Null_Success` passed in the Oracle artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#152](https://github.com/dotnetprojects/Migrator.NET/issues/152) Exception "This is currently not supported by the migrator see issue #44. You need to use NOT NULL for a PK column." occurs | Verified on master; closed | The old issue-44 exception is absent; SQLite supplies single-PK NOT NULL and supports nullable composite members in existing regressions. | | [#161](https://github.com/dotnetprojects/Migrator.NET/issues/161) Microsoft SQLite: FK integrity issue when using AddTable (by e.g. using AddColumn) | Fix in PRs #173/#174; await merge | SQLite FK settings restored after success/failure; integrity checked before commit; rebuild dependencies guarded. Regression coverage uses both driver paths. | -| [#162](https://github.com/dotnetprojects/Migrator.NET/issues/162) DbType.Time is not implemented | Partial; keep open | SQL Server native TIME metadata/defaults and TimeSpan binding verified live. PostgreSQL native TIME metadata/default parsing now has a regression in PR #174; current CI pending. Oracle and SqlServer2005 retain documented historical representations; no universal native time claim. | +| [#162](https://github.com/dotnetprojects/Migrator.NET/issues/162) DbType.Time is not implemented | Partial; keep open | SQL Server native TIME metadata/defaults and TimeSpan binding verified live. PostgreSQL native TIME metadata/default parsing now has a regression in PR #174; passed live PostgreSQL CI in run 35742976746. Oracle and SqlServer2005 retain documented historical representations; no universal native time claim. | | [#164](https://github.com/dotnetprojects/Migrator.NET/issues/164) PostgreTransform Provider does not quote IncludeColumns for reserved names | Historically closed; relevant baseline test verified | `AddIndex_IncludeColumnsWithReservedWord_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | | [#165](https://github.com/dotnetprojects/Migrator.NET/issues/165) Included columns not quoted in Postgre | Duplicate verified; closed | Duplicates #164; PostgreSQL included-column quoting is covered by the live metadata regression. | | [#167](https://github.com/dotnetprojects/Migrator.NET/issues/167) Get columns in postgre should not quote table name | Historically closed; relevant baseline test verified | `AddIndex_TableNameIsReservedWord_Succeeds` passed in the PostgreSQL artifact of master run 35715528132 (1 case(s)). This verifies the named scenario; broader edge cases are not inferred. | diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index af015450..817c70c9 100644 --- a/docs/migration-framework-comparison.md +++ b/docs/migration-framework-comparison.md @@ -316,7 +316,7 @@ Potential Migrator improvements, **not implemented-feature claims**: ## Validation and maintenance -The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. The pinned upgrade revision passed **93 unit tests and 184 SQLite tests, with no skips**, after rebuilding the solution. Test counts reflect replacement of assertion-free tests with behavioral checks. The packed/installed tool previously passed offline SQL, migration, status and rollback smoke checks. The provider fixes at `bdc8ac3` passed all eleven database/unit jobs and the coverage gate in [run 35737814671](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737814671). Concurrent-runner tests passed on SQL Server, PostgreSQL, MySQL and MariaDB at `bb88165` in [run 35741656276](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35741656276). The later PostgreSQL metadata/time changes require their own PR checks; a green earlier revision is not evidence for a later revision. +The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. The pinned upgrade revision passed **93 unit tests and 184 SQLite tests, with no skips**, after rebuilding the solution. Test counts reflect replacement of assertion-free tests with behavioral checks. The packed/installed tool previously passed offline SQL, migration, status and rollback smoke checks. The provider fixes at `bdc8ac3` passed all eleven database/unit jobs and the coverage gate in [run 35737814671](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35737814671). Concurrent-runner tests passed on SQL Server, PostgreSQL, MySQL and MariaDB at `bb88165` in [run 35741656276](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35741656276). The pinned revision `bc35e0e`, including PostgreSQL metadata/time changes, passed all eleven database/unit jobs and the coverage gate in [run 35743265022](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35743265022). A green earlier revision is not evidence for a later revision. This is not a complete implementation of the upgrade plan: SQL preview supports a structured subset; offline CLI rejects profiles/maintenance; full client-script dialects, remaining metadata/legacy ownership cases and broader deployment regressions remain work in progress. SQL Server GO splitting and explicit Oracle legacy sequence cleanup are implemented. See the [81-issue inventory](issue-audit.md) for verified closures and incomplete audit items. The operation inventory maps normal API method families to fluent/context entry points, but does not establish every overload/provider combination through execution. Competitors were reviewed through documentation/source, **not executed in a comparative harness**.