From 281628047df688a5d46c223a2076f12bc89ef2f3 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 14:52:28 +0200 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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**. From d9998be70bfe51cf1ead48a2ace3c0b760a135b9 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 18:09:26 +0200 Subject: [PATCH 18/20] Introduce v13 named table constraints and structured metadata Add primary-key and unique-constraint definitions with ordered columns; rename Unique to UniqueConstraint. Render named keys/checks in complete table definitions, reject malformed keys before DDL and expose fluent equivalents. Add pure dialect constraint rendering and atom-level identifier quoting. Add structured readers for SQLite, SQL Server, PostgreSQL, Oracle and MySQL/MariaDB, with explicit diagnostics for unsupported readers. Replace SQLite regex-based constraint-name recovery with a tokenizer handling quoted identifiers and nested expressions. Keep legacy flag removal as the next isolated step. Document each implemented breaking API change and current reader limitations in the 12.1-to-13 migration guide. Validation: solution build, 93 unit tests and 190 SQLite tests passed; live provider matrix is required on the PR. --- docs/fluent-operation-coverage.json | 3 +- docs/migration-guide-12.1-to-13.md | 61 +++++ src/Migrator.Tests/ProviderCorrectionTests.cs | 3 +- .../Generic/Generic_GetColumnsTestsBase.cs | 27 ++ ...SQLiteTransformationProvider_GetUniques.cs | 6 +- src/Migrator.Tests/SchemaConstraintTests.cs | 79 ++++++ src/Migrator/Framework/CheckConstraint.cs | 3 +- .../Framework/Fluent/FluentMigration.cs | 1 + .../Framework/Fluent/MigrationBuilder.cs | 3 + src/Migrator/Framework/Fluent/Operations.cs | 18 +- .../Framework/ForeignKeyConstraint.cs | 3 +- src/Migrator/Framework/IDialect.cs | 3 + .../Framework/ITransformationProvider.cs | 3 + src/Migrator/Framework/TableConstraint.cs | 24 ++ src/Migrator/Framework/Unique.cs | 8 - .../Providers/ConstraintMetadataReader.cs | 101 +++++++ src/Migrator/Providers/Dialect.cs | 25 ++ .../Impl/SQLite/Models/SQLiteTableInfo.cs | 2 +- .../Impl/SQLite/SQLiteConstraintParser.cs | 184 +++++++++++++ .../SQLite/SQLiteTransformationProvider.cs | 256 ++++-------------- .../Providers/NoOpTransformationProvider.cs | 2 + .../Providers/TransformationProvider.cs | 32 ++- 22 files changed, 619 insertions(+), 228 deletions(-) create mode 100644 docs/migration-guide-12.1-to-13.md create mode 100644 src/Migrator.Tests/SchemaConstraintTests.cs create mode 100644 src/Migrator/Framework/TableConstraint.cs delete mode 100644 src/Migrator/Framework/Unique.cs create mode 100644 src/Migrator/Providers/ConstraintMetadataReader.cs create mode 100644 src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs diff --git a/docs/fluent-operation-coverage.json b/docs/fluent-operation-coverage.json index b57cf8c9..11022870 100644 --- a/docs/fluent-operation-coverage.json +++ b/docs/fluent-operation-coverage.json @@ -69,5 +69,6 @@ "MigrationApplied": "Explicit Context history/transaction APIs; not schema expressions", "MigrationUnApplied": "Explicit Context history/transaction APIs; not schema expressions", "IsMigrationApplied": "Explicit Context history/transaction APIs; not schema expressions", - "ExecuteSchemaBuilder": "Execute.WithProvider(p => p.ExecuteSchemaBuilder(legacyBuilder))" + "ExecuteSchemaBuilder": "Execute.WithProvider(p => p.ExecuteSchemaBuilder(legacyBuilder))", + "GetTableConstraints": "Schema.Table(table).ConstraintDefinitions()" } diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md new file mode 100644 index 00000000..5b83b385 --- /dev/null +++ b/docs/migration-guide-12.1-to-13.md @@ -0,0 +1,61 @@ +# Migrating from 12.1 to 13 + +Version 13 is a breaking release. This guide is maintained alongside the implementation; items explicitly marked planned are not available yet. Do not run a changed migration history against production without validating the upgrade on a restored database. + +## Schema model (implementation in progress) + +Columns describe data type, length, precision/scale, nullability, identity generation and defaults. Primary keys, unique constraints, foreign keys and checks belong to the table. Indexes are separate schema objects: a unique index is not automatically a unique constraint. + +The v13 target API accepts complete named constraint definitions when creating a table, and returns the same kinds of definitions from metadata inspection. Key column order is significant. A composite UNIQUE constraint must never mark each member column as individually unique. Altering a column must not infer that its table constraints should be removed. + +Planned removal: ColumnProperty.PrimaryKey, PrimaryKeyNonClustered, PrimaryKeyWithIdentity, Unique and Indexed; unnamed fluent PrimaryKey()/Unique() shortcuts; name-based ownership inference. The replacement examples and exact supported-provider behavior are added with the corresponding implementation commits below. + +## Implemented breaking changes + +### `Unique` is renamed to `UniqueConstraint` + +Replace `new Unique { Name = "UQ_Users_Email", KeyColumns = ["Email"] }` with `new UniqueConstraint("UQ_Users_Email", "Email")`. When importing both `System.Data` and `DotNetProjects.Migrator.Framework`, use an alias for the latter's `UniqueConstraint` (ADO.NET also defines that name). + +### Explicit table keys and complete constraint definitions + +```csharp +Database.AddTable("Users", + new Column("TenantId", DbType.Int32), + new Column("Id", DbType.Int32), + new Column("Email", DbType.String, 200), + new PrimaryKeyConstraint("PK_Users", "TenantId", "Id"), + new UniqueConstraint("UQ_Users_Email", "TenantId", "Email"), + new CheckConstraint("CK_Users_Id", "Id > 0")); +``` + +The supplied key order is preserved. Explicit primary-key definitions make their columns non-nullable without mutating the caller's column objects. This also rejects NULL in a composite SQLite primary key; old flag-based composite SQLite keys allowed NULL. Do not combine a constraint object with legacy primary-key flags. SQLite identity requires a single INTEGER primary key and rejects incompatible combinations instead of silently removing identity. + +Fluent equivalent: append `.WithPrimaryKey("PK_Users", "TenantId", "Id")`, `.WithUniqueConstraint("UQ_Users_Email", "TenantId", "Email")`, or `.WithCheckConstraint("CK_Users_Id", "Id > 0")` to the table builder. Each is part of the complete table definition. + +### Structured constraint inspection + +Use `Database.GetTableConstraints("Users")`, or `Schema.Table("Users").ConstraintDefinitions()`, then select `PrimaryKeyConstraint`, `UniqueConstraint`, `ForeignKeyConstraint` or `CheckConstraint`. Key column order belongs to the constraint. A unique index stays in index metadata. SQLite returns `Name == null` for unnamed legacy constraints; a backing autoindex name is not an invented constraint name. + +Initial structured readers cover SQLite, SQL Server, PostgreSQL, Oracle, MySQL and MariaDB. Other readers explicitly throw `NotSupportedException` until implemented. MySQL identifies the primary key as `PRIMARY` regardless of a supplied symbolic name. Quoted qualified Oracle/MySQL lookups are currently rejected explicitly. These limitations must not be interpreted as empty metadata. + +### Custom provider and dialect implementations + +`ITransformationProvider` now requires `GetTableConstraints(string)`. Return accurate typed definitions, including ordered key members, or throw `NotSupportedException`; do not return an empty array for an unsupported reader. `IDialect` adds `QuoteIdentifier(string)` for one identifier atom and `GetTableConstraintSql(TableConstraint)` for pure SQL rendering. Implementations derived from `Dialect` inherit defaults. Constraint names containing quote delimiters are escaped; a dot within a constraint name is not a schema separator. + +## Provider authors and dialects (design) + +Keep SQL generation independent of a live connection. A dialect defines identifier quoting, type/literal rendering and SQL capabilities. Metadata readers inspect existing schema; execution manages commands, transactions and history. Neither preview nor a SQL generator may query or mutate the database. + +The current provider surface mixes these concerns. The v13 implementation is staged to preserve testable provider behavior while replacing authoring APIs. Unsupported combinations must fail explicitly before DDL, not disappear from generated SQL. + +## Design references + +Reviewed 2026-09-22: + +- [EF Core CreateTableOperation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.migrations.operations.createtableoperation?view=efcore-10.0) separates columns, primary key, unique constraints, checks and foreign keys. +- [FluentMigrator ColumnDefinition](https://github.com/fluentmigrator/fluentmigrator/blob/main/src/FluentMigrator.Abstractions/Model/ColumnDefinition.cs) still carries constraint flags; its expression/generator separation is useful, but its column model is not the target here. +- [Alembic operations](https://alembic.sqlalchemy.org/en/latest/ops.html) distinguish named table constraints from column alteration and use explicit batch reconstruction for SQLite. + +## Additional v13 candidates + +Evaluate typed schema-qualified identifiers, explicit literal versus SQL-expression defaults, ordered constraint metadata, deterministic constraint naming, SQLite constraint parsing without regular-expression guesses, typed provider capabilities, and removal of obsolete duplicate authoring APIs. These are candidates, not claims of implemented functionality. diff --git a/src/Migrator.Tests/ProviderCorrectionTests.cs b/src/Migrator.Tests/ProviderCorrectionTests.cs index 0e845eaf..424a525a 100644 --- a/src/Migrator.Tests/ProviderCorrectionTests.cs +++ b/src/Migrator.Tests/ProviderCorrectionTests.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; using System.Linq; using DotNetProjects.Migrator; using DotNetProjects.Migrator.Framework; @@ -16,7 +17,7 @@ [Test] public void InlineConstraintNamesAndReservedUniqueColumnsAreQuoted() using var connection = new SqliteConnection("Data Source=:memory:"); connection.Open(); using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); provider.AddTable("QuotedConstraints", new Column("select", DbType.Int32), - new Unique { Name = "unique name", KeyColumns = new[] { "select" } }, + new UniqueConstraint { Name = "unique name", KeyColumns = new[] { "select" } }, new CheckConstraint("check name", "\"select\" > 0")); provider.ExecuteNonQuery("INSERT INTO QuotedConstraints VALUES (1)"); Assert.That(Assert.Throws(() => provider.ExecuteNonQuery("INSERT INTO QuotedConstraints VALUES (1)")).InnerException, Is.TypeOf()); diff --git a/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs index 22c3681f..6be8fd58 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs @@ -8,6 +8,33 @@ namespace Migrator.Tests.Providers.Generic; public abstract class Generic_GetColumnsTestsBase : TransformationProviderBase { + [Test] + public void NamedTableConstraintsEnforceCompositeKeysAndReturnOrderedMetadata() + { + Provider.AddTable("NamedConstraintModel", + new Column("IdA", DbType.Int32), new Column("IdB", DbType.Int32), new Column("Amount", DbType.Int32), + new PrimaryKeyConstraint("PK_NamedModel", "IdB", "IdA"), + new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_NamedModel", "IdA", "Amount"), + new CheckConstraint("CK_NamedModel", "Amount >= 0")); + Provider.Insert("NamedConstraintModel", new[] { "IdA", "IdB", "Amount" }, new object[] { 1, 2, 3 }); + Provider.Insert("NamedConstraintModel", new[] { "IdA", "IdB", "Amount" }, new object[] { 1, 3, 4 }); + var dialect = Provider.Dialect; + if (dialect is DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteDialect or + DotNetProjects.Migrator.Providers.Impl.SqlServer.SqlServerDialect or + DotNetProjects.Migrator.Providers.Impl.PostgreSQL.PostgreSQLDialect or + DotNetProjects.Migrator.Providers.Impl.Oracle.OracleDialect or + DotNetProjects.Migrator.Providers.Impl.Mysql.MysqlDialect) + { + var constraints = Provider.GetTableConstraints("NamedConstraintModel"); + Assert.That(constraints.OfType().Single().KeyColumns.Select(c => c.ToUpperInvariant()), Is.EqualTo(new[] { "IDB", "IDA" })); + Assert.That(constraints.OfType().Single().KeyColumns.Select(c => c.ToUpperInvariant()), Is.EqualTo(new[] { "IDA", "AMOUNT" })); + Assert.That(constraints.OfType().Any(c => c.Name.ToUpperInvariant() == "CK_NAMEDMODEL"), Is.True); + } + else Assert.Throws(() => Provider.GetTableConstraints("NamedConstraintModel")); + // On PostgreSQL a failing statement aborts this test's transaction, so check one complete-key violation last. + Assert.Catch(() => Provider.Insert("NamedConstraintModel", new[] { "IdA", "IdB", "Amount" }, new object[] { 1, 2, 5 })); + } + [Test] public void CompositeUniqueDoesNotMarkItsIndividualColumnsUnique() { diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs index d4727474..6ec7fb5d 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs @@ -1,4 +1,5 @@ using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; using System.Linq; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Impl.SQLite; @@ -28,7 +29,8 @@ public void GetUniques_Success() Provider.AddTable(tableNameA, new Column(property1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(property2, DbType.Int32, ColumnProperty.Unique), + new Column(property2, DbType.Int32), + new UniqueConstraint("UniqueConstraint0", property2), new Column(property3, DbType.Int32), new Column(property4, DbType.Int32), new Column(property5, DbType.Int32) @@ -54,7 +56,7 @@ public void GetUniques_Success() Assert.That(sql, Does.Contain("CONSTRAINT UniqueConstraint1 UNIQUE (Property3)")); Assert.That(sql, Does.Contain("CONSTRAINT UniqueConstraint2 UNIQUE (Property4, Property5)")); - Assert.That(sql, Does.Contain("CONSTRAINT sqlite_autoindex_TableA_1 UNIQUE (Property2)")); + Assert.That(sql, Does.Contain("CONSTRAINT UniqueConstraint0 UNIQUE (Property2)")); var retrievedUniqueIndex1 = indexes.Single(x => x.Name == uniqueIndexName1); diff --git a/src/Migrator.Tests/SchemaConstraintTests.cs b/src/Migrator.Tests/SchemaConstraintTests.cs new file mode 100644 index 00000000..43d9da1b --- /dev/null +++ b/src/Migrator.Tests/SchemaConstraintTests.cs @@ -0,0 +1,79 @@ +using System; +using DotNetProjects.Migrator; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Fluent; +using DotNetProjects.Migrator.Providers; +using NUnit.Framework; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; + +namespace Migrator.Tests; + +[Category("SQLite")] +public class SchemaConstraintTests +{ + [Test] + public void NamedCompositeConstraintsPreserveOrderAndEnforceWholeKeys() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + var first = new Column("First", DbType.Int32); + provider.AddTable("OrderedKeys", first, new Column("Second", DbType.Int32), new Column("Label", DbType.String), + new PrimaryKeyConstraint("Primary key", "Second", "First"), + new UniqueConstraint("Unique pair", "First", "Label"), + new CheckConstraint("Check label", "length(Label) > 0 AND instr(Label, ',') = 0")); + var constraints = provider.GetTableConstraints("OrderedKeys"); + Assert.That(constraints.OfType().Single().KeyColumns, Is.EqualTo(new[] { "Second", "First" })); + Assert.That(constraints.OfType().Single().Name, Is.EqualTo("Unique pair")); + Assert.That(constraints.OfType().Single().CheckConstraintString, Does.Contain("instr(Label, ',')")); + Assert.That(first.ColumnProperty, Is.EqualTo(ColumnProperty.None), "Creating a key must not mutate caller-owned columns."); + provider.ExecuteNonQuery("INSERT INTO OrderedKeys VALUES (1, 2, 'a'), (1, 3, 'b')"); + Assert.Catch(() => provider.ExecuteNonQuery("INSERT INTO OrderedKeys VALUES (1, 2, 'c')")); + Assert.Catch(() => provider.ExecuteNonQuery("INSERT INTO OrderedKeys VALUES (1, 4, 'a')")); + Assert.Catch(() => provider.ExecuteNonQuery("INSERT INTO OrderedKeys VALUES (NULL, 4, 'x')")); + Assert.Catch(() => provider.ExecuteNonQuery("INSERT INTO OrderedKeys VALUES (9, 9, 'a,b')")); + } + + [Test] + public void NamedIdentityKeyAndQuotedNamesRoundTrip() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.AddTable("IdentityKeys", new Column("Id", DbType.Int32, ColumnProperty.Identity), + new PrimaryKeyConstraint("PK \"quoted\"", "Id")); + provider.ExecuteNonQuery("INSERT INTO IdentityKeys DEFAULT VALUES"); + Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT Id FROM IdentityKeys")), Is.EqualTo(1)); + Assert.That(provider.GetTableConstraints("IdentityKeys").Single().Name, Is.EqualTo("PK \"quoted\"")); + } + + [Test] + public void UnnamedLegacyConstraintsHaveNoInventedNamesAndUniqueIndexesStaySeparate() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.ExecuteNonQuery("CREATE TABLE \"Old'Table\" (Id INTEGER PRIMARY KEY, Value TEXT UNIQUE CHECK (length(Value) > 0))"); + provider.ExecuteNonQuery("CREATE UNIQUE INDEX ExtraIndex ON \"Old'Table\"(Value)"); + var constraints = provider.GetTableConstraints("Old'Table"); + Assert.That(constraints.Length, Is.EqualTo(3)); + Assert.That(constraints.All(c => c.Name == null), Is.True); + Assert.That(constraints.OfType().Single().KeyColumns, Is.EqualTo(new[] { "Value" })); + } + + [Test] + public void FluentNamedDefinitionsAreCompleteBeforeExecution() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + var builder = new MigrationBuilder(); + builder.Create.Table("FluentKeys").WithColumn("Id").AsInt32() + .WithPrimaryKey("PK_FluentKeys", "Id").WithUniqueConstraint("UQ_FluentKeys", "Id"); + Assert.That(builder.Preview(new SqlGenerationContext(ProviderTypes.SQLite)).Single(), Does.Contain("CONSTRAINT \"PK_FluentKeys\" PRIMARY KEY")); + builder.Apply(provider); + Assert.That(provider.GetTableConstraints("FluentKeys").Length, Is.EqualTo(2)); + } + + [Test] + public void InvalidKeyDefinitionsFailBeforeCreatingTheTable() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + Assert.Throws(() => provider.AddTable("InvalidKey", new Column("Id", DbType.Int32), new PrimaryKeyConstraint("PK_Invalid", "Missing"))); + Assert.That(provider.TableExists("InvalidKey"), Is.False); + } +} diff --git a/src/Migrator/Framework/CheckConstraint.cs b/src/Migrator/Framework/CheckConstraint.cs index d682fa9a..0845f89d 100644 --- a/src/Migrator/Framework/CheckConstraint.cs +++ b/src/Migrator/Framework/CheckConstraint.cs @@ -3,7 +3,7 @@ namespace DotNetProjects.Migrator.Framework; /// /// Currently only used for SQLite /// -public class CheckConstraint : IDbField +public class CheckConstraint : TableConstraint { public CheckConstraint() { } @@ -22,5 +22,4 @@ public CheckConstraint(string name, string checkConstraintText) /// /// Gets or sets the name of the CHECK constraint. /// - public string Name { get; set; } } diff --git a/src/Migrator/Framework/Fluent/FluentMigration.cs b/src/Migrator/Framework/Fluent/FluentMigration.cs index 9bb7a5b5..a99c1b32 100644 --- a/src/Migrator/Framework/Fluent/FluentMigration.cs +++ b/src/Migrator/Framework/Fluent/FluentMigration.cs @@ -68,6 +68,7 @@ public sealed class TableInspector(ITransformationProvider provider, string tabl public Column Column(string name) => provider.GetColumnByName(table, name); public Index[] Indexes() => provider.GetIndexes(table); public ForeignKeyConstraint[] ForeignKeys() => provider.GetForeignKeyConstraints(table); + public TableConstraint[] ConstraintDefinitions() => provider.GetTableConstraints(table); public string[] Constraints() => (provider as DotNetProjects.Migrator.Providers.TransformationProvider)?.GetConstraints(table) ?? throw new NotSupportedException("Provider does not expose constraint enumeration."); public int ContentSize(string column) => provider.GetColumnContentSize(table, column); public int? NullableContentSize(string column) => provider.GetNullableColumnContentSize(table, column); diff --git a/src/Migrator/Framework/Fluent/MigrationBuilder.cs b/src/Migrator/Framework/Fluent/MigrationBuilder.cs index ae1a2023..4b17c682 100644 --- a/src/Migrator/Framework/Fluent/MigrationBuilder.cs +++ b/src/Migrator/Framework/Fluent/MigrationBuilder.cs @@ -62,6 +62,9 @@ public sealed class TableBuilder internal TableBuilder(MigrationBuilder builder, string name) => builder.Add(() => new CreateTableOperation(name, engine, fields.Select(Definitions.Copy).ToArray())); public TableBuilder WithColumn(string name) { current = new Column(name, DbType.String, ColumnProperty.Null); fields.Add(current); return this; } public TableBuilder WithFields(params IDbField[] values) { fields.AddRange(values.Select(Definitions.Copy)); return this; } + public TableBuilder WithPrimaryKey(string name, params string[] columns) { fields.Add(new PrimaryKeyConstraint(name, columns)); return this; } + public TableBuilder WithUniqueConstraint(string name, params string[] columns) { fields.Add(new UniqueConstraint(name, columns)); return this; } + public TableBuilder WithCheckConstraint(string name, string expression) { fields.Add(new CheckConstraint(name, expression)); return this; } public TableBuilder WithEngine(string value) { engine = value; return this; } private Column Current => current ?? throw new InvalidOperationException("Call WithColumn first."); public TableBuilder OfType(DbType type) { Current.Type = type; return this; } diff --git a/src/Migrator/Framework/Fluent/Operations.cs b/src/Migrator/Framework/Fluent/Operations.cs index 6232abc6..655690c2 100644 --- a/src/Migrator/Framework/Fluent/Operations.cs +++ b/src/Migrator/Framework/Fluent/Operations.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; using System.Linq; using DotNetProjects.Migrator.Providers; using Index = DotNetProjects.Migrator.Framework.Index; @@ -30,12 +31,22 @@ public override void Apply(ITransformationProvider p) public override MigrationOperation Reverse() => new RemoveOperation(RemoveKind.Table, Table); public override string ToSql(SqlGenerationContext c) { - if (Engine != null || Fields.Any(f => f is not Column)) throw new NotSupportedException("Preview this table's constraints as separate operations."); - var columns = Fields.Cast().Select(Definitions.CopyColumn).ToArray(); + if (Engine != null || Fields.Any(f => f is not (Column or PrimaryKeyConstraint or UniqueConstraint or CheckConstraint))) throw new NotSupportedException("This table contains an unsupported preview definition."); + var columns = Fields.OfType().Select(Definitions.CopyColumn).ToArray(); + var primary = Fields.OfType().SingleOrDefault(); + if (primary != null) + { + if (columns.Any(x => x.IsPrimaryKey)) throw new MigrationException("Do not combine primary-key flags and constraints."); + foreach (var column in columns.Where(x => primary.KeyColumns.Contains(x.Name))) + column.ColumnProperty = (column.ColumnProperty & ~ColumnProperty.Null) | ColumnProperty.NotNull; + if (c.Provider == ProviderTypes.SQLite && columns.Any(x => x.IsIdentity)) + throw new NotSupportedException("Named SQLite identity-key preview requires the complete table generator."); + } var pks = columns.Where(x => x.IsPrimaryKey).ToArray(); if (pks.Length > 1) foreach (var column in pks) column.ColumnProperty &= ~ColumnProperty.PrimaryKey; var definitions = columns.Select(c.Column).ToList(); if (pks.Length > 1) definitions.Add($"PRIMARY KEY ({string.Join(", ", pks.Select(x => c.Quote(x.Name)))})"); + definitions.AddRange(Fields.OfType().Select(c.Dialect.GetTableConstraintSql)); c.AddTable(Table, columns); return $"CREATE TABLE {c.Table(Table)} ({string.Join(", ", definitions)});"; } @@ -244,7 +255,8 @@ public static class Definitions Column c => CopyColumn(c), Index i => new Index { Name = i.Name, Unique = i.Unique, Clustered = i.Clustered, KeyColumns = (string[])i.KeyColumns.Clone(), IncludeColumns = (string[])i.IncludeColumns.Clone(), FilterItems = i.FilterItems.Select(f => new DotNetProjects.Migrator.Providers.Models.Indexes.FilterItem { ColumnName = f.ColumnName, Filter = f.Filter, Value = f.Value }).ToList() }, ForeignKeyConstraint f => new ForeignKeyConstraint(f.Name, f.ParentTable, (string[])f.ParentColumns.Clone(), f.ChildTable, (string[])f.ChildColumns.Clone()) { OnDelete = f.OnDelete, OnUpdate = f.OnUpdate, Match = f.Match, Id = f.Id }, - Unique u => new Unique { Name = u.Name, KeyColumns = (string[])u.KeyColumns.Clone() }, + PrimaryKeyConstraint k => new PrimaryKeyConstraint(k.Name, k.KeyColumns) { NonClustered = k.NonClustered }, + UniqueConstraint u => new UniqueConstraint { Name = u.Name, KeyColumns = (string[])u.KeyColumns.Clone() }, CheckConstraint c => new CheckConstraint(c.Name, c.CheckConstraintString), _ => throw new NotSupportedException($"Cannot snapshot {field.GetType().Name}.") }; diff --git a/src/Migrator/Framework/ForeignKeyConstraint.cs b/src/Migrator/Framework/ForeignKeyConstraint.cs index 1a5af872..15d9d4a2 100644 --- a/src/Migrator/Framework/ForeignKeyConstraint.cs +++ b/src/Migrator/Framework/ForeignKeyConstraint.cs @@ -1,6 +1,6 @@ namespace DotNetProjects.Migrator.Framework; -public class ForeignKeyConstraint : IDbField +public class ForeignKeyConstraint : TableConstraint { public ForeignKeyConstraint() { } @@ -19,7 +19,6 @@ public ForeignKeyConstraint(string name, string parentTable, string[] parentcolu /// Currently used for SQLite /// public int? Id { get; set; } - public string Name { get; set; } public string ParentTable { get; set; } public string[] ParentColumns { get; set; } public string ChildTable { get; set; } diff --git a/src/Migrator/Framework/IDialect.cs b/src/Migrator/Framework/IDialect.cs index ffe0cf78..a36c6052 100644 --- a/src/Migrator/Framework/IDialect.cs +++ b/src/Migrator/Framework/IDialect.cs @@ -4,6 +4,9 @@ namespace DotNetProjects.Migrator.Framework; public interface IDialect { + string QuoteIdentifier(string name); + string GetTableConstraintSql(TableConstraint constraint); + int MaxKeyLength { get; } int MaxFieldNameLength { get; } bool ColumnNameNeedsQuote { get; } diff --git a/src/Migrator/Framework/ITransformationProvider.cs b/src/Migrator/Framework/ITransformationProvider.cs index 63fd04cd..b1c597bd 100644 --- a/src/Migrator/Framework/ITransformationProvider.cs +++ b/src/Migrator/Framework/ITransformationProvider.cs @@ -10,6 +10,9 @@ namespace DotNetProjects.Migrator.Framework; /// public interface ITransformationProvider : IDisposable { + /// Read named table constraints with ordered key columns; indexes are separate objects. + TableConstraint[] GetTableConstraints(string table); + /// /// Get this provider or a NoOp provider if you are not running in the context of 'provider'. /// diff --git a/src/Migrator/Framework/TableConstraint.cs b/src/Migrator/Framework/TableConstraint.cs new file mode 100644 index 00000000..0baf6ce0 --- /dev/null +++ b/src/Migrator/Framework/TableConstraint.cs @@ -0,0 +1,24 @@ +namespace DotNetProjects.Migrator.Framework; + +/// A table-level constraint. Column order is significant for keys. +public abstract class TableConstraint : IDbField +{ + public string Name { get; set; } +} + +public sealed class PrimaryKeyConstraint : TableConstraint +{ + public PrimaryKeyConstraint() { } + public PrimaryKeyConstraint(string name, params string[] columns) + { Name = name; KeyColumns = (string[])columns.Clone(); } + public string[] KeyColumns { get; set; } = []; + public bool NonClustered { get; set; } +} + +public class UniqueConstraint : TableConstraint +{ + public UniqueConstraint() { } + public UniqueConstraint(string name, params string[] columns) + { Name = name; KeyColumns = (string[])columns.Clone(); } + public string[] KeyColumns { get; set; } = []; +} diff --git a/src/Migrator/Framework/Unique.cs b/src/Migrator/Framework/Unique.cs deleted file mode 100644 index 6ad3ce93..00000000 --- a/src/Migrator/Framework/Unique.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DotNetProjects.Migrator.Framework; - -public class Unique : IDbField -{ - public string Name { get; set; } - - public string[] KeyColumns { get; set; } -} diff --git a/src/Migrator/Providers/ConstraintMetadataReader.cs b/src/Migrator/Providers/ConstraintMetadataReader.cs new file mode 100644 index 00000000..25c6e54c --- /dev/null +++ b/src/Migrator/Providers/ConstraintMetadataReader.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Impl.SqlServer; +using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; +using DotNetProjects.Migrator.Providers.Impl.Oracle; +using DotNetProjects.Migrator.Providers.Impl.Mysql; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; + +namespace DotNetProjects.Migrator.Providers; + +internal static class ConstraintMetadataReader +{ + public static TableConstraint[] Read(TransformationProvider provider, string table) + { + string sql; + var parameterTable = table; + string schema = null; + var oracle = provider.Dialect is OracleDialect; + if (provider.Dialect is SqlServerDialect) + sql = @"SELECT kc.name, kc.type, c.name, ic.key_ordinal, CAST(NULL AS nvarchar(max)) + FROM sys.key_constraints kc JOIN sys.index_columns ic ON ic.object_id=kc.parent_object_id AND ic.index_id=kc.unique_index_id + JOIN sys.columns c ON c.object_id=ic.object_id AND c.column_id=ic.column_id + WHERE kc.parent_object_id=OBJECT_ID(@table) AND ic.key_ordinal>0 + UNION ALL SELECT name, 'C', NULL, 0, definition FROM sys.check_constraints WHERE parent_object_id=OBJECT_ID(@table) + ORDER BY 1,4"; + else if (provider.Dialect is PostgreSQLDialect) + { + parameterTable = provider.QuoteTableNameIfRequired(table); + sql = @"SELECT c.conname, c.contype::text, a.attname, k.ordinality, CASE WHEN c.contype='c' THEN pg_get_expr(c.conbin,c.conrelid) END + FROM pg_constraint c LEFT JOIN LATERAL unnest(c.conkey) WITH ORDINALITY k(attnum,ordinality) ON c.contype<>'c' + LEFT JOIN pg_attribute a ON a.attrelid=c.conrelid AND a.attnum=k.attnum + WHERE c.conrelid=to_regclass(@table) AND c.contype IN ('p','u','c') ORDER BY c.conname,k.ordinality"; + } + else if (oracle || provider.Dialect is MysqlDialect) + { + // Quoted identifiers containing a dot need a structured name API rather than ambiguous splitting. + var parts = table.Split('.'); + if (parts.Length > 2 || parts.Any(p => p.Contains('"') || p.Contains('`') || p.Contains('['))) + throw new NotSupportedException("Quoted qualified constraint lookup is not implemented for this provider."); + parameterTable = oracle ? parts[^1].ToUpperInvariant() : parts[^1]; + schema = parts.Length == 2 ? (oracle ? parts[0].ToUpperInvariant() : parts[0]) : null; + sql = oracle ? @"SELECT c.CONSTRAINT_NAME,c.CONSTRAINT_TYPE,k.COLUMN_NAME,k.POSITION,c.SEARCH_CONDITION_VC + FROM ALL_CONSTRAINTS c LEFT JOIN ALL_CONS_COLUMNS k ON k.OWNER=c.OWNER AND k.CONSTRAINT_NAME=c.CONSTRAINT_NAME AND c.CONSTRAINT_TYPE IN ('P','U') + WHERE c.TABLE_NAME=:table AND c.OWNER=COALESCE(:schema,SYS_CONTEXT('USERENV','CURRENT_SCHEMA')) AND c.CONSTRAINT_TYPE IN ('P','U','C') + ORDER BY c.CONSTRAINT_NAME,k.POSITION" + : @"SELECT c.CONSTRAINT_NAME,c.CONSTRAINT_TYPE,k.COLUMN_NAME,k.ORDINAL_POSITION,ch.CHECK_CLAUSE + FROM information_schema.TABLE_CONSTRAINTS c LEFT JOIN information_schema.KEY_COLUMN_USAGE k + ON k.CONSTRAINT_SCHEMA=c.CONSTRAINT_SCHEMA AND k.TABLE_NAME=c.TABLE_NAME AND k.CONSTRAINT_NAME=c.CONSTRAINT_NAME + LEFT JOIN information_schema.CHECK_CONSTRAINTS ch ON ch.CONSTRAINT_SCHEMA=c.CONSTRAINT_SCHEMA AND ch.CONSTRAINT_NAME=c.CONSTRAINT_NAME + WHERE c.TABLE_NAME=@table AND c.TABLE_SCHEMA=COALESCE(@schema,DATABASE()) AND c.CONSTRAINT_TYPE IN ('PRIMARY KEY','UNIQUE','CHECK') + ORDER BY c.CONSTRAINT_NAME,k.ORDINAL_POSITION"; + } + else throw new NotSupportedException("Structured constraint inspection is not implemented for " + provider.Dialect.GetType().Name + "."); + + var constraints = new List(); + using (var command = provider.CreateCommand()) + { + AddParameter(command, "table", parameterTable); + if (oracle || provider.Dialect is MysqlDialect) AddParameter(command, "schema", schema); + using var reader = provider.ExecuteQuery(command, sql); + string lastName = null; + TableConstraint current = null; + var keys = new List(); + void Complete() + { + if (current is PrimaryKeyConstraint pk) pk.KeyColumns = keys.ToArray(); + if (current is UniqueConstraint unique) unique.KeyColumns = keys.ToArray(); + if (current != null) constraints.Add(current); + } + while (reader.Read()) + { + var name = reader.GetString(0); + if (name != lastName) + { + Complete(); keys.Clear(); lastName = name; + current = reader.GetString(1).Trim().ToUpperInvariant() switch + { + "P" or "PK" or "PRIMARY KEY" => new PrimaryKeyConstraint { Name = name }, + "U" or "UQ" or "UNIQUE" => new UniqueConstraint { Name = name }, + "C" or "CHECK" => new CheckConstraint(name, reader.IsDBNull(4) ? null : reader.GetString(4)), + _ => throw new MigrationException("Unknown catalog constraint type.") + }; + } + if (!reader.IsDBNull(2)) keys.Add(reader.GetString(2)); + } + Complete(); + } + constraints.AddRange(provider.GetForeignKeyConstraints(table)); + return constraints.ToArray(); + } + + private static void AddParameter(IDbCommand command, string name, object value) + { + var parameter = command.CreateParameter(); parameter.ParameterName = name; + parameter.DbType = DbType.String; parameter.Value = value ?? DBNull.Value; + command.Parameters.Add(parameter); + } +} diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index 8a3fcf11..06728b1f 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; using System.Globalization; using System.Linq; using DotNetProjects.Migrator.Framework; @@ -37,6 +38,30 @@ protected Dialect() RegisterProperty(ColumnProperty.PrimaryKeyNonClustered, " NONCLUSTERED"); } + /// Render a named table constraint without accessing a database. + public virtual string GetTableConstraintSql(TableConstraint constraint) + { + if (string.IsNullOrWhiteSpace(constraint.Name)) throw new MigrationException("A constraint name is required."); + string Keys(string[] columns) => string.Join(", ", columns.Select(name => ColumnNameNeedsQuote || IsReservedWord(name) ? QuoteIdentifier(name) : name)); + var body = constraint switch + { + PrimaryKeyConstraint p when p.NonClustered && !SupportsNonClustered => throw new System.NotSupportedException("This dialect does not support nonclustered primary keys."), + PrimaryKeyConstraint p => $"PRIMARY KEY{(p.NonClustered ? " NONCLUSTERED" : "")} ({Keys(p.KeyColumns)})", + UniqueConstraint u => $"UNIQUE ({Keys(u.KeyColumns)})", + CheckConstraint c when !string.IsNullOrWhiteSpace(c.CheckConstraintString) => $"CHECK ({c.CheckConstraintString})", + _ => throw new System.NotSupportedException($"No table-constraint SQL generator for {constraint.GetType().Name}.") + }; + return $"CONSTRAINT {QuoteIdentifier(constraint.Name)} {body}"; + } + + /// Quote one identifier atom, escaping its delimiter; never split a name on dots. + public virtual string QuoteIdentifier(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Identifier must not be empty.", nameof(name)); + var closing = QuoteTemplate[^1].ToString(); + return string.Format(CultureInfo.InvariantCulture, QuoteTemplate, name.Replace(closing, closing + closing)); + } + public virtual int MaxKeyLength { get { return 900; } diff --git a/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs b/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs index c839b034..96ae0e4a 100644 --- a/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +++ b/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs @@ -33,7 +33,7 @@ public class SQLiteTableInfo /// /// Gets or sets the unique definitions. /// - public List Uniques { get; set; } = []; + public List Uniques { get; set; } = []; /// /// Gets or sets the check constraint definitions. diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs new file mode 100644 index 00000000..777153ee --- /dev/null +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DotNetProjects.Migrator.Framework; + +namespace DotNetProjects.Migrator.Providers.Impl.SQLite; + +// Tokenize DDL rather than matching identifiers/expressions with regular expressions. +internal static class SQLiteConstraintParser +{ + private sealed record Token(string Text, int Start, int End, bool Quoted = false) + { + public bool Is(string value) => !Quoted && Text.Equals(value, StringComparison.OrdinalIgnoreCase); + } + + public static TableConstraint[] Parse(string sql) + { + var tokens = Tokenize(sql); + var start = tokens.FindIndex(t => t.Is("(")); + if (start < 0) throw new MigrationException("SQLite CREATE TABLE has no column definition list."); + var end = Close(tokens, start); + var result = new List(); + var first = start + 1; + var depth = 0; + for (var i = first; i <= end; i++) + { + if (i == end || (depth == 0 && tokens[i].Is(","))) + { + ParseDefinition(sql, tokens.GetRange(first, i - first), result); + first = i + 1; + } + else if (tokens[i].Is("(")) depth++; + else if (tokens[i].Is(")")) depth--; + } + return result.ToArray(); + } + + private static void ParseDefinition(string sql, List tokens, List result) + { + if (tokens.Count == 0) return; + var tableLevel = tokens[0].Is("CONSTRAINT") || tokens[0].Is("PRIMARY") || tokens[0].Is("UNIQUE") || tokens[0].Is("FOREIGN") || tokens[0].Is("CHECK"); + var column = tableLevel ? null : tokens[0].Text; + string name = null; + for (var i = tableLevel ? 0 : 1; i < tokens.Count; i++) + { + if (tokens[i].Is("CONSTRAINT")) + { + if (++i >= tokens.Count) throw new MigrationException("Missing SQLite constraint name."); + name = tokens[i].Text; + } + else if (tokens[i].Is("PRIMARY") && i + 1 < tokens.Count && tokens[i + 1].Is("KEY")) + { + i++; + var keys = column == null ? Columns(tokens, ref i) : new[] { column }; + result.Add(new PrimaryKeyConstraint(name, keys)); name = null; + } + else if (tokens[i].Is("UNIQUE")) + { + var keys = column == null ? Columns(tokens, ref i) : new[] { column }; + result.Add(new UniqueConstraint(name, keys)); name = null; + } + else if (tokens[i].Is("CHECK")) + { + if (i + 1 >= tokens.Count || !tokens[i + 1].Is("(")) throw new MigrationException("Missing CHECK expression."); + var close = Close(tokens, i + 1); + result.Add(new CheckConstraint(name, sql[tokens[i + 1].End..tokens[close].Start])); + i = close; name = null; + } + else if (tokens[i].Is("FOREIGN") && i + 1 < tokens.Count && tokens[i + 1].Is("KEY")) + { + i++; + var children = Columns(tokens, ref i); + if (++i >= tokens.Count || !tokens[i].Is("REFERENCES")) throw new MigrationException("Missing foreign-key reference."); + result.Add(Reference(tokens, ref i, name, children)); name = null; + } + else if (tokens[i].Is("REFERENCES") && column != null) + { + result.Add(Reference(tokens, ref i, name, new[] { column })); name = null; + } + else if (tokens[i].Is("(")) i = Close(tokens, i); + } + } + + private static ForeignKeyConstraint Reference(List tokens, ref int index, string name, string[] children) + { + if (++index >= tokens.Count) throw new MigrationException("Missing referenced table."); + var parent = tokens[index].Text; + string[] parents = []; + if (index + 1 < tokens.Count && tokens[index + 1].Is("(")) parents = Columns(tokens, ref index); + var fk = new ForeignKeyConstraint(name, parent, parents, null, children) { OnDelete = "NO ACTION", OnUpdate = "NO ACTION" }; + while (index + 1 < tokens.Count) + { + if (tokens[index + 1].Is("ON")) + { + index += 2; + if (index + 1 >= tokens.Count) throw new MigrationException("Incomplete foreign-key action."); + var delete = tokens[index].Is("DELETE"); + if (!delete && !tokens[index].Is("UPDATE")) throw new MigrationException("Unknown foreign-key action."); + var action = tokens[++index].Text.ToUpperInvariant(); + if (action is "SET" or "NO") + { + if (++index >= tokens.Count) throw new MigrationException("Incomplete foreign-key action."); + action += " " + tokens[index].Text.ToUpperInvariant(); + } + if (delete) fk.OnDelete = action; else fk.OnUpdate = action; + } + else if (tokens[index + 1].Is("MATCH")) + { + index += 2; + if (index >= tokens.Count) throw new MigrationException("Incomplete MATCH clause."); + fk.Match = tokens[index].Text; + } + else break; + } + return fk; + } + + private static string[] Columns(List tokens, ref int index) + { + if (++index >= tokens.Count || !tokens[index].Is("(")) throw new MigrationException("Missing constraint column list."); + var end = Close(tokens, index); + var columns = new List(); + for (var i = index + 1; i < end; i += 2) + { + columns.Add(tokens[i].Text); + if (i + 1 < end && !tokens[i + 1].Is(",")) throw new NotSupportedException("Constraint column modifiers require explicit schema support."); + } + index = end; + if (columns.Count == 0) throw new MigrationException("Constraint column list is empty."); + return columns.ToArray(); + } + + private static int Close(List tokens, int open) + { + var depth = 0; + for (var i = open; i < tokens.Count; i++) + { + if (tokens[i].Is("(")) depth++; + if (tokens[i].Is(")") && --depth == 0) return i; + } + throw new MigrationException("Unbalanced SQLite definition."); + } + + private static List Tokenize(string sql) + { + var result = new List(); + for (var i = 0; i < sql.Length;) + { + if (char.IsWhiteSpace(sql[i])) { i++; continue; } + if (i + 1 < sql.Length && sql[i] == '-' && sql[i + 1] == '-') { while (i < sql.Length && sql[i] != '\n') i++; continue; } + if (i + 1 < sql.Length && sql[i] == '/' && sql[i + 1] == '*') + { + var end = sql.IndexOf("*/", i + 2, StringComparison.Ordinal); + if (end < 0) throw new MigrationException("Unterminated SQL comment."); + i = end + 2; continue; + } + var start = i; + if (sql[i] is '\'' or '"' or '`' or '[') + { + var close = sql[i++] == '[' ? ']' : sql[start]; + var text = new System.Text.StringBuilder(); var closed = false; + while (i < sql.Length) + { + var ch = sql[i++]; + if (ch == close) + { + if (i < sql.Length && sql[i] == close) { text.Append(close); i++; } + else { closed = true; break; } + } + else text.Append(ch); + } + if (!closed) throw new MigrationException("Unterminated quoted SQL token."); + result.Add(new Token(text.ToString(), start, i, true)); + } + else if (sql[i] is '(' or ')' or ',' or '.') { result.Add(new Token(sql[i++].ToString(), start, i)); } + else + { + while (i < sql.Length && !char.IsWhiteSpace(sql[i]) && sql[i] is not ('(' or ')' or ',' or '.' or '\'' or '"' or '`' or '[')) i++; + result.Add(new Token(sql[start..i], start, i)); + } + } + return result; + } +} diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 6c3b5b6a..cfadbafc 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; using System.Globalization; using System.Linq; using System.Text; @@ -123,18 +124,23 @@ public string GetSqlCreateTableScript(string table) { string sqlCreateTableScript = null; - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, string.Format("SELECT sql FROM sqlite_master WHERE type='table' AND lower(name)=lower('{0}')", table))) - { - if (reader.Read()) - { - sqlCreateTableScript = reader.IsDBNull(0) ? null : (string)reader[0]; - } - } + using var cmd = CreateCommand(); + var parameter = cmd.CreateParameter(); parameter.ParameterName = "@name"; parameter.Value = table; cmd.Parameters.Add(parameter); + using var reader = ExecuteQuery(cmd, "SELECT sql FROM sqlite_master WHERE type='table' AND name=@name COLLATE NOCASE"); + if (reader.Read()) sqlCreateTableScript = reader.IsDBNull(0) ? null : reader.GetString(0); return sqlCreateTableScript; } + public override TableConstraint[] GetTableConstraints(string table) + { + var script = GetSqlCreateTableScript(table); + if (string.IsNullOrWhiteSpace(script)) throw new MigrationException("Table does not exist: " + table); + var constraints = SQLiteConstraintParser.Parse(script); + foreach (var foreignKey in constraints.OfType()) foreignKey.ChildTable = table; + return constraints; + } + public override ForeignKeyConstraint[] GetForeignKeyConstraints(string tableName) { List foreignKeyConstraints = []; @@ -166,61 +172,16 @@ public override ForeignKeyConstraint[] GetForeignKeyConstraints(string tableName return []; } - var createTableScript = GetSqlCreateTableScript(tableName); - // GeneratedRegex - var regEx = new Regex(@"CONSTRAINT\s+\w+\s+FOREIGN\s+KEY\s*\([^)]+\)\s+REFERENCES\s+[\w""]+\s*\([^)]+\)"); - var matchesCollection = regEx.Matches(createTableScript); - var fkParts = matchesCollection.Cast().ToList().Where(x => x.Success).Select(x => x.Value).ToList(); - - if (fkParts.Count != foreignKeyConstraints.Count) - { - throw new Exception($"Cannot extract all foreign keys out of the create table script in SQLite. Did you use a name as foreign key constraint for all constraints in table '{tableName}' in this or older migrations?"); - } - - List foreignKeyExtracts = []; - - foreach (var fkPart in fkParts) - { - var regexParenthesis = new Regex(@"\(([^)]+)\)"); - var parenthesisContents = regexParenthesis.Matches(fkPart).Cast().Select(x => x.Groups[1].Value).ToList(); - - if (parenthesisContents.Count != 2) - { - throw new Exception("Cannot extract parenthesis of foreign key constraint"); - } - - var foreignKeyExtract = new ForeignKeyExtract() - { - ChildColumnNames = parenthesisContents[0].Split(',').Select(x => x.Trim()).ToList(), - ForeignKeyString = fkPart, - ParentColumnNames = parenthesisContents[1].Split(',').Select(x => x.Trim()).ToList(), - }; - - var foreignKeyConstraintNameRegex = new Regex(@"CONSTRAINT\s+(\w+)\s+FOREIGN\s+KEY"); - var foreignKeyNameMatch = foreignKeyConstraintNameRegex.Match(fkPart); - - if (!foreignKeyNameMatch.Success) - { - throw new Exception("Could not extract the foreign key constraint name"); - } - - foreignKeyExtract.ForeignKeyName = foreignKeyNameMatch.Groups[1].Value; - - foreignKeyExtracts.Add(foreignKeyExtract); - } - - foreach (var foreignKeyConstraint in foreignKeyConstraints) + var declared = GetTableConstraints(tableName).OfType().ToList(); + foreach (var foreignKey in foreignKeyConstraints) { - foreach (var foreignKeyExtract in foreignKeyExtracts) - { - if ( - foreignKeyExtract.ChildColumnNames.SequenceEqual(foreignKeyConstraint.ChildColumns) && - foreignKeyExtract.ParentColumnNames.SequenceEqual(foreignKeyConstraint.ParentColumns) - ) - { - foreignKeyConstraint.Name = foreignKeyExtract.ForeignKeyName; - } - } + var definition = declared.FirstOrDefault(candidate => + candidate.ChildColumns.SequenceEqual(foreignKey.ChildColumns, StringComparer.OrdinalIgnoreCase) && + candidate.ParentTable.Equals(foreignKey.ParentTable, StringComparison.OrdinalIgnoreCase) && + (candidate.ParentColumns.Length == 0 || candidate.ParentColumns.SequenceEqual(foreignKey.ParentColumns, StringComparer.OrdinalIgnoreCase))); + if (definition == null) throw new MigrationException("Cannot match a SQLite foreign key to its declaration."); + foreignKey.Name = definition.Name; + declared.Remove(definition); } return foreignKeyConstraints.ToArray(); @@ -783,7 +744,7 @@ public override void AddUniqueConstraint(string name, string table, params strin throw new MigrationException("A unique constraint with the same name already exists."); } - var uniqueConstraint = new Unique() { KeyColumns = columns, Name = name }; + var uniqueConstraint = new UniqueConstraint() { KeyColumns = columns, Name = name }; sqliteTableInfo.Uniques.Add(uniqueConstraint); RecreateTable(sqliteTableInfo); @@ -1489,6 +1450,22 @@ public override void AddTable(string name, string engine, params IDbField[] fiel .Select(column => column.CopyDefinition()) .ToArray(); + var explicitKeys = fields.OfType().ToArray(); + if (explicitKeys.Length > 1) throw new MigrationException("A table can have only one primary key."); + var explicitKey = explicitKeys.SingleOrDefault(); + if (explicitKey != null) + { + ValidateKeyColumns(explicitKey.Name, explicitKey.KeyColumns, columns); + if (explicitKey.NonClustered) throw new NotSupportedException("SQLite does not support nonclustered primary keys."); + if (columns.Any(c => c.IsPrimaryKey)) throw new MigrationException("Do not combine column primary-key flags with a primary-key constraint."); + foreach (var column in columns.Where(c => explicitKey.KeyColumns.Contains(c.Name))) + column.ColumnProperty = (column.ColumnProperty & ~ColumnProperty.Null) | ColumnProperty.NotNull; + var identities = columns.Where(c => c.IsIdentity).ToArray(); + if (identities.Length != 0 && (identities.Length != 1 || explicitKey.KeyColumns.Length != 1 || explicitKey.KeyColumns[0] != identities[0].Name || _dialect.GetTypeName(identities[0].Type) != "INTEGER")) + throw new MigrationException("SQLite identity requires one INTEGER primary-key column."); + } + foreach (var unique in fields.OfType()) ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); + var pks = GetPrimaryKeys(columns); var hasCompoundPrimaryKey = pks.Count > 1; @@ -1512,11 +1489,19 @@ public override void AddTable(string name, string engine, params IDbField[] fiel column.ColumnProperty &= ~ColumnProperty.Identity; } - var mapper = _dialect.GetAndMapColumnProperties(column); + var mapped = column.CopyDefinition(); + if (explicitKey != null && mapped.IsIdentity) mapped.ColumnProperty &= ~ColumnProperty.Identity; + var mapper = _dialect.GetAndMapColumnProperties(mapped); columnProviders.Add(mapper); } - var columnsAndIndexes = JoinColumnsAndIndexes(columnProviders); + var columnSql = columnProviders.Select((mapper, index) => + explicitKey != null && columns[index].IsIdentity + ? mapper.ColumnSql + $" CONSTRAINT {_dialect.QuoteIdentifier(explicitKey.Name)} PRIMARY KEY AUTOINCREMENT" + : mapper.ColumnSql); + var columnsAndIndexes = string.Join(", ", columnSql); + if (explicitKey != null && !columns.Any(c => c.IsIdentity)) + columnsAndIndexes += ", " + _dialect.GetTableConstraintSql(explicitKey); var table = _dialect.TableNameNeedsQuote ? _dialect.Quote(name) : QuoteTableNameIfRequired(name); StringBuilder stringBuilder = new(); @@ -1530,7 +1515,7 @@ public override void AddTable(string name, string engine, params IDbField[] fiel // Uniques - var uniques = fields.Where(x => x is Unique).Cast().ToArray(); + var uniques = fields.Where(x => x is UniqueConstraint).Cast().ToArray(); foreach (var u in uniques) { @@ -1736,91 +1721,8 @@ public override void RemoveAllIndexes(string tableName) RecreateTable(sqliteInfoTable); } - public List GetUniques(string tableName) - { - if (!TableExists(tableName)) - { - throw new Exception($"Table '{tableName}' does not exist."); - } - - var regEx = new Regex(@"(?<=,)\s*(CONSTRAINT\s+\w+\s+)?UNIQUE\s*\(\s*[\w\s,]+\s*\)\s*(?=,|\s*\))"); - var regExConstraintName = new Regex(@"(?<=CONSTRAINT\s+)\w+(?=\s+)"); - var regExParenthesis = new Regex(@"(?<=\().+(?=\))"); - - List uniques = []; - - var pragmaIndexListItems = GetPragmaIndexListItems(tableName); - - // Here we filter for origin u and unique while in "GetIndexes()" we exclude them. - // If "pk" is set then it was added by using a primary key. If so this is handled by "GetColumns()". - // If "c" is set it was created by using CREATE INDEX. - var uniqueConstraints = pragmaIndexListItems.Where(x => x.Unique && x.Origin == "u") - .ToList(); - - foreach (var uniqueConstraint in uniqueConstraints) - { - var indexInfos = GetPragmaIndexInfo(uniqueConstraint.Name); - - var columns = indexInfos.OrderBy(x => x.SeqNo) - .Select(x => x.Name) - .ToArray(); - - var unique = new Unique - { - Name = uniqueConstraint.Name, - KeyColumns = columns - }; - - uniques.Add(unique); - } - - var createScript = GetSqlCreateTableScript(tableName); - - var matches = regEx.Matches(createScript); - if (matches.Count == 0) - { - return []; - } - - var constraintNames = matches - .OfType() - .Where(x => x.Success && !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => x.Value.Trim()) - .ToList(); - - // We can only use the ones containing a starting with CONSTRAINT - var matchesHavingName = constraintNames.Where(x => x.StartsWith("CONSTRAINT")).ToList(); - - foreach (var constraintString in matchesHavingName) - { - var constraintNameMatch = regExConstraintName.Match(constraintString); - - if (!constraintNameMatch.Success) - { - throw new Exception("Cannot extract constraint name. Please file an issue"); - } - - var constraintName = constraintNameMatch.Value; - - var parenthesisMatch = regExParenthesis.Match(constraintString); - - if (!parenthesisMatch.Success) - { - throw new Exception("Cannot extract parenthesis content for UNIQUE constraint. Please file an issue"); - } - - var columns = parenthesisMatch.Value.Split(',').Select(x => x.Trim()).ToList(); - - var unique = uniques.Where(x => x.KeyColumns.SequenceEqual(columns)).SingleOrDefault(); - - if (unique != null) - { - unique.Name = constraintName; - } - } - - return uniques; - } + public List GetUniques(string tableName) => GetTableConstraints(tableName) + .OfType().Where(c => c.Name != null).ToList(); public List GetPragmaIndexInfo(string indexNameNotQuoted) { @@ -1968,57 +1870,7 @@ public override void CopyDataFromTableToTable(string sourceTableName, List GetCheckConstraints(string tableName) - { - if (!TableExists(tableName)) - { - throw new Exception($"Table '{tableName}' does not exist."); - } - - var checkConstraintRegex = new Regex(@"(?<=,)[^,]+\s+[^,]+check[^,]+(?=[,|\)])", RegexOptions.IgnoreCase); - var braceContentRegex = new Regex(@"(?<=^\().+(?=\)$)"); - - var script = GetSqlCreateTableScript(tableName); - - var matches = checkConstraintRegex.Matches(script); - - if (matches == null) - { - return []; - } - - var checkStrings = matches.OfType() - .Where(x => x.Success) - .Select(x => x.Value) - .ToList(); - - List checkConstraints = []; - - foreach (var checkString in checkStrings) - { - var splitted = checkString.Trim().Split(' ') - .Select(x => x.Trim()) - .ToList(); - - if (!splitted[0].Equals("CONSTRAINT", StringComparison.OrdinalIgnoreCase) || !splitted[2].Equals("CHECK", StringComparison.OrdinalIgnoreCase)) - { - throw new Exception($"Cannot parse check constraint in table {tableName}"); - } - - var checkConstraintStringWithBraces = string.Join(" ", splitted.Skip(3)).Trim(); - var checkConstraintString = braceContentRegex.Match(checkConstraintStringWithBraces); - - var checkConstraint = new CheckConstraint - { - Name = splitted[1], - CheckConstraintString = checkConstraintString.Value - }; - - checkConstraints.Add(checkConstraint); - } - - return checkConstraints; - } + public List GetCheckConstraints(string tableName) => GetTableConstraints(tableName).OfType().ToList(); protected override void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) { diff --git a/src/Migrator/Providers/NoOpTransformationProvider.cs b/src/Migrator/Providers/NoOpTransformationProvider.cs index 145918df..a808049f 100644 --- a/src/Migrator/Providers/NoOpTransformationProvider.cs +++ b/src/Migrator/Providers/NoOpTransformationProvider.cs @@ -14,6 +14,8 @@ namespace DotNetProjects.Migrator.Providers; /// public class NoOpTransformationProvider : ITransformationProvider { + public TableConstraint[] GetTableConstraints(string table) => []; + public static readonly NoOpTransformationProvider Instance = new NoOpTransformationProvider(); private NoOpTransformationProvider() diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index c6ee88f1..a7d21719 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -20,6 +20,7 @@ using System; using System.Collections.Generic; using System.Data; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; using System.Data.Common; using System.IO; using System.Linq; @@ -219,6 +220,8 @@ public virtual ForeignKeyConstraint[] GetForeignKeyConstraints(string table) return [.. constraints]; } + public virtual TableConstraint[] GetTableConstraints(string table) => ConstraintMetadataReader.Read(this, table); + public virtual string[] GetConstraints(string table) { var constraints = new List(); @@ -387,11 +390,6 @@ public virtual void AddView(string name, string tableName, params IViewElement[] /// Columns public virtual void AddTable(string name, params IDbField[] columns) { - if (this is not SQLiteTransformationProvider && columns.Any(x => x is CheckConstraint)) - { - throw new MigrationException($"{nameof(CheckConstraint)}s are currently only supported in SQLite."); - } - // Most databases don't have the concept of a storage engine, so default is to not use it. AddTable(name, null, columns); } @@ -404,7 +402,18 @@ public virtual void AddTable(string name, params IDbField[] columns) /// the database storage engine to use public virtual void AddTable(string name, string engine, params IDbField[] fields) { - var columns = fields.Where(x => x is Column).Cast().ToArray(); + var columns = fields.OfType().Select(c => c.CopyDefinition()).ToArray(); + var primaryKeys = fields.OfType().ToArray(); + if (primaryKeys.Length > 1) throw new MigrationException("A table can have only one primary key."); + var explicitKey = primaryKeys.SingleOrDefault(); + if (explicitKey != null) + { + if (columns.Any(c => c.IsPrimaryKey)) throw new MigrationException("Do not combine column primary-key flags with a primary-key constraint."); + ValidateKeyColumns(explicitKey.Name, explicitKey.KeyColumns, columns); + foreach (var column in columns.Where(c => explicitKey.KeyColumns.Contains(c.Name))) + column.ColumnProperty = (column.ColumnProperty & ~ColumnProperty.Null) | ColumnProperty.NotNull; + } + foreach (var unique in fields.OfType()) ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); var pks = GetPrimaryKeys(columns); var compoundPrimaryKey = pks.Count > 1; @@ -425,6 +434,8 @@ public virtual void AddTable(string name, string engine, params IDbField[] field } var columnsAndIndexes = JoinColumnsAndIndexes(columnProviders); + foreach (var constraint in fields.OfType().Where(c => c is not ForeignKeyConstraint)) + columnsAndIndexes += ", " + Dialect.GetTableConstraintSql(constraint); AddTable(name, engine, columnsAndIndexes); @@ -448,6 +459,15 @@ public virtual void AddTable(string name, string engine, params IDbField[] field } } + protected static void ValidateKeyColumns(string name, string[] keys, Column[] columns) + { + if (string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name is required."); + if (keys == null || keys.Length == 0 || keys.Any(string.IsNullOrWhiteSpace) || keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != keys.Length) + throw new MigrationException("A key needs distinct, non-empty column names."); + if (keys.Any(key => !columns.Any(c => c.Name.Equals(key, StringComparison.OrdinalIgnoreCase)))) + throw new MigrationException("A constraint references a column that is absent from the table definition."); + } + protected virtual string GetPrimaryKeyname(string tableName) { return "PK_" + tableName; From dba4db43015e25e0669b19d437d5bcbb60d0a823 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 18:18:09 +0200 Subject: [PATCH 19/20] Avoid reserved Oracle bind names in structured constraint metadata The live Oracle matrix reproduced ORA-01745 because :table is a reserved bind identifier. Use lookup_table and lookup_schema consistently in SQL and command parameters for each catalog query. The ordered composite-constraint regression exercises this path on Oracle and the other supported catalogs. Validation: solution build and all 93 unit tests pass locally. Oracle integration validation runs in the PR matrix; the preceding head passed the other ten test jobs. --- src/Migrator/Providers/ConstraintMetadataReader.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Migrator/Providers/ConstraintMetadataReader.cs b/src/Migrator/Providers/ConstraintMetadataReader.cs index 25c6e54c..e374c1ea 100644 --- a/src/Migrator/Providers/ConstraintMetadataReader.cs +++ b/src/Migrator/Providers/ConstraintMetadataReader.cs @@ -23,8 +23,8 @@ public static TableConstraint[] Read(TransformationProvider provider, string tab sql = @"SELECT kc.name, kc.type, c.name, ic.key_ordinal, CAST(NULL AS nvarchar(max)) FROM sys.key_constraints kc JOIN sys.index_columns ic ON ic.object_id=kc.parent_object_id AND ic.index_id=kc.unique_index_id JOIN sys.columns c ON c.object_id=ic.object_id AND c.column_id=ic.column_id - WHERE kc.parent_object_id=OBJECT_ID(@table) AND ic.key_ordinal>0 - UNION ALL SELECT name, 'C', NULL, 0, definition FROM sys.check_constraints WHERE parent_object_id=OBJECT_ID(@table) + WHERE kc.parent_object_id=OBJECT_ID(@lookup_table) AND ic.key_ordinal>0 + UNION ALL SELECT name, 'C', NULL, 0, definition FROM sys.check_constraints WHERE parent_object_id=OBJECT_ID(@lookup_table) ORDER BY 1,4"; else if (provider.Dialect is PostgreSQLDialect) { @@ -32,7 +32,7 @@ public static TableConstraint[] Read(TransformationProvider provider, string tab sql = @"SELECT c.conname, c.contype::text, a.attname, k.ordinality, CASE WHEN c.contype='c' THEN pg_get_expr(c.conbin,c.conrelid) END FROM pg_constraint c LEFT JOIN LATERAL unnest(c.conkey) WITH ORDINALITY k(attnum,ordinality) ON c.contype<>'c' LEFT JOIN pg_attribute a ON a.attrelid=c.conrelid AND a.attnum=k.attnum - WHERE c.conrelid=to_regclass(@table) AND c.contype IN ('p','u','c') ORDER BY c.conname,k.ordinality"; + WHERE c.conrelid=to_regclass(@lookup_table) AND c.contype IN ('p','u','c') ORDER BY c.conname,k.ordinality"; } else if (oracle || provider.Dialect is MysqlDialect) { @@ -44,13 +44,13 @@ FROM pg_constraint c LEFT JOIN LATERAL unnest(c.conkey) WITH ORDINALITY k(attnum schema = parts.Length == 2 ? (oracle ? parts[0].ToUpperInvariant() : parts[0]) : null; sql = oracle ? @"SELECT c.CONSTRAINT_NAME,c.CONSTRAINT_TYPE,k.COLUMN_NAME,k.POSITION,c.SEARCH_CONDITION_VC FROM ALL_CONSTRAINTS c LEFT JOIN ALL_CONS_COLUMNS k ON k.OWNER=c.OWNER AND k.CONSTRAINT_NAME=c.CONSTRAINT_NAME AND c.CONSTRAINT_TYPE IN ('P','U') - WHERE c.TABLE_NAME=:table AND c.OWNER=COALESCE(:schema,SYS_CONTEXT('USERENV','CURRENT_SCHEMA')) AND c.CONSTRAINT_TYPE IN ('P','U','C') + WHERE c.TABLE_NAME=:lookup_table AND c.OWNER=COALESCE(:lookup_schema,SYS_CONTEXT('USERENV','CURRENT_SCHEMA')) AND c.CONSTRAINT_TYPE IN ('P','U','C') ORDER BY c.CONSTRAINT_NAME,k.POSITION" : @"SELECT c.CONSTRAINT_NAME,c.CONSTRAINT_TYPE,k.COLUMN_NAME,k.ORDINAL_POSITION,ch.CHECK_CLAUSE FROM information_schema.TABLE_CONSTRAINTS c LEFT JOIN information_schema.KEY_COLUMN_USAGE k ON k.CONSTRAINT_SCHEMA=c.CONSTRAINT_SCHEMA AND k.TABLE_NAME=c.TABLE_NAME AND k.CONSTRAINT_NAME=c.CONSTRAINT_NAME LEFT JOIN information_schema.CHECK_CONSTRAINTS ch ON ch.CONSTRAINT_SCHEMA=c.CONSTRAINT_SCHEMA AND ch.CONSTRAINT_NAME=c.CONSTRAINT_NAME - WHERE c.TABLE_NAME=@table AND c.TABLE_SCHEMA=COALESCE(@schema,DATABASE()) AND c.CONSTRAINT_TYPE IN ('PRIMARY KEY','UNIQUE','CHECK') + WHERE c.TABLE_NAME=@lookup_table AND c.TABLE_SCHEMA=COALESCE(@lookup_schema,DATABASE()) AND c.CONSTRAINT_TYPE IN ('PRIMARY KEY','UNIQUE','CHECK') ORDER BY c.CONSTRAINT_NAME,k.ORDINAL_POSITION"; } else throw new NotSupportedException("Structured constraint inspection is not implemented for " + provider.Dialect.GetType().Name + "."); @@ -58,8 +58,8 @@ FROM information_schema.TABLE_CONSTRAINTS c LEFT JOIN information_schema.KEY_COL var constraints = new List(); using (var command = provider.CreateCommand()) { - AddParameter(command, "table", parameterTable); - if (oracle || provider.Dialect is MysqlDialect) AddParameter(command, "schema", schema); + AddParameter(command, "lookup_table", parameterTable); + if (oracle || provider.Dialect is MysqlDialect) AddParameter(command, "lookup_schema", schema); using var reader = provider.ExecuteQuery(command, sql); string lastName = null; TableConstraint current = null; From 3962b37eec7d41dce3b5fc5f50ceef3add3137c9 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 18:23:09 +0200 Subject: [PATCH 20/20] Preserve named SQLite primary keys through table reconstruction Carry named primary-key definitions in SQLite table snapshots and emit them when rebuilding instead of deriving key order from legacy column flags. Copy columns before clearing transitional flags, preserve physical column order when altering attributes, and retain identity key names and sequence high-water values. Update rename and explicit primary-key removal paths. Reject removal of a member of a named key before changing the original table, and document the explicit removal sequence in the 12.1-to-13 migration guide. Validation: solution build, 93 unit tests and 192 SQLite tests pass. New regressions verify composite key order, constraint names, persisted rows, duplicate/null enforcement, explicit removal, physical column order and identity sequence continuity. --- docs/migration-guide-12.1-to-13.md | 13 +++++++ src/Migrator.Tests/SchemaConstraintTests.cs | 36 +++++++++++++++++++ .../Impl/SQLite/Models/SQLiteTableInfo.cs | 3 ++ .../SQLite/SQLiteTransformationProvider.cs | 29 +++++++++++---- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index 5b83b385..4db5ad0f 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -59,3 +59,16 @@ Reviewed 2026-09-22: ## Additional v13 candidates Evaluate typed schema-qualified identifiers, explicit literal versus SQL-expression defaults, ordered constraint metadata, deterministic constraint naming, SQLite constraint parsing without regular-expression guesses, typed provider capabilities, and removal of obsolete duplicate authoring APIs. These are candidates, not claims of implemented functionality. + + +### SQLite alterations preserve named primary keys + +Rebuilding a table now retains an explicitly named primary key and its declared +column order. Changing a column definition does not implicitly remove that key. +To drop a column belonging to a named primary key, first call +`RemovePrimaryKey(table)`, then remove the column, and explicitly create any +replacement key. A failed attempt leaves the original table intact. + +Rebuilds also retain physical column order for tables with named primary keys, +including when a column's type or size changes. Identity rebuilds retain the +constraint name and the sequence high-water mark. diff --git a/src/Migrator.Tests/SchemaConstraintTests.cs b/src/Migrator.Tests/SchemaConstraintTests.cs index 43d9da1b..24bd868a 100644 --- a/src/Migrator.Tests/SchemaConstraintTests.cs +++ b/src/Migrator.Tests/SchemaConstraintTests.cs @@ -57,6 +57,42 @@ public void UnnamedLegacyConstraintsHaveNoInventedNamesAndUniqueIndexesStaySepar Assert.That(constraints.OfType().Single().KeyColumns, Is.EqualTo(new[] { "Value" })); } + [Test] + public void RebuildPreservesNamedKeyOrderAndColumnOrder() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.AddTable("RebuiltKeys", new Column("First", DbType.Int32), new Column("Second", DbType.Int32), + new Column("Label", DbType.String, 20), new PrimaryKeyConstraint("PK ordered", "Second", "First")); + provider.ExecuteNonQuery("INSERT INTO RebuiltKeys VALUES (1, 2, 'kept')"); + provider.ChangeColumn("RebuiltKeys", new Column("First", DbType.Int64)); + var key = provider.GetTableConstraints("RebuiltKeys").OfType().Single(); + Assert.That(key.Name, Is.EqualTo("PK ordered")); + Assert.That(key.KeyColumns, Is.EqualTo(new[] { "Second", "First" })); + Assert.That(((DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteTransformationProvider)provider).GetPragmaTableInfoItems("RebuiltKeys").OrderBy(c => c.Cid).Select(c => c.Name), Is.EqualTo(new[] { "First", "Second", "Label" })); + Assert.That(provider.ExecuteScalar("SELECT Label FROM RebuiltKeys WHERE First=1 AND Second=2"), Is.EqualTo("kept")); + Assert.Catch(() => provider.ExecuteNonQuery("INSERT INTO RebuiltKeys VALUES (1, 2, 'duplicate')")); + Assert.Catch(() => provider.ExecuteNonQuery("INSERT INTO RebuiltKeys VALUES (NULL, 3, 'null')")); + Assert.Throws(() => provider.RemoveColumn("RebuiltKeys", "First")); + Assert.That(provider.ColumnExists("RebuiltKeys", "First"), Is.True); + provider.RemovePrimaryKey("RebuiltKeys"); + Assert.That(provider.GetTableConstraints("RebuiltKeys").OfType(), Is.Empty); + provider.ExecuteNonQuery("INSERT INTO RebuiltKeys VALUES (1, 2, 'allowed')"); + } + + [Test] + public void RebuildPreservesNamedIdentityAndSequenceHighWater() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.AddTable("RebuiltIdentity", new Column("Id", DbType.Int32, ColumnProperty.Identity), + new Column("Value", DbType.String, 20), new PrimaryKeyConstraint("PK identity", "Id")); + provider.ExecuteNonQuery("INSERT INTO RebuiltIdentity VALUES (40, 'removed')"); + provider.ExecuteNonQuery("DELETE FROM RebuiltIdentity"); + provider.ChangeColumn("RebuiltIdentity", new Column("Value", DbType.String, 40)); + provider.ExecuteNonQuery("INSERT INTO RebuiltIdentity (Value) VALUES ('next')"); + Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT Id FROM RebuiltIdentity")), Is.EqualTo(41)); + Assert.That(provider.GetTableConstraints("RebuiltIdentity").OfType().Single().Name, Is.EqualTo("PK identity")); + } + [Test] public void FluentNamedDefinitionsAreCompleteBeforeExecution() { diff --git a/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs b/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs index 96ae0e4a..3a4c8ea4 100644 --- a/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +++ b/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs @@ -15,6 +15,9 @@ public class SQLiteTableInfo /// public List Columns { get; set; } = []; + /// The named primary key, with members in declared key order. + public PrimaryKeyConstraint PrimaryKey { get; set; } + /// /// Gets or sets the indexes of a table. /// diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index cfadbafc..511be5e2 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -463,6 +463,9 @@ public override void RemoveColumn(string tableName, string column) var sqliteInfoMainTable = GetSQLiteTableInfo(tableName); + if (sqliteInfoMainTable.PrimaryKey?.KeyColumns.Any(x => x.Equals(column, StringComparison.OrdinalIgnoreCase)) == true) + throw new MigrationException("Remove the named primary-key constraint before removing one of its columns."); + var checkConstraints = sqliteInfoMainTable.CheckConstraints; if (checkConstraints.Any(x => x.CheckConstraintString.Contains(column, StringComparison.OrdinalIgnoreCase))) @@ -616,6 +619,9 @@ public override void RenameColumn(string tableName, string oldColumnName, string var column = sqliteTableInfo.Columns.First(x => x.Name.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase)); column.Name = newColumnName; + if (sqliteTableInfo.PrimaryKey != null) + sqliteTableInfo.PrimaryKey.KeyColumns = sqliteTableInfo.PrimaryKey.KeyColumns + .Select(x => x.Equals(oldColumnName, StringComparison.OrdinalIgnoreCase) ? newColumnName : x).ToArray(); foreach (var foreignKey in sqliteTableInfo.ForeignKeys) { @@ -770,12 +776,19 @@ public SQLiteTableInfo GetSQLiteTableInfo(string tableName) { TableNameMapping = new MappingInfo { OldName = tableName, NewName = tableName }, Columns = GetColumns(tableName).ToList(), + PrimaryKey = GetTableConstraints(tableName).OfType().SingleOrDefault(c => c.Name != null), ForeignKeys = GetForeignKeyConstraints(tableName).ToList(), Indexes = GetIndexes(tableName).ToList(), Uniques = GetUniques(tableName).ToList(), CheckConstraints = GetCheckConstraints(tableName) }; + if (sqliteTable.PrimaryKey != null) + { + var columnOrder = GetPragmaTableInfoItems(tableName).ToDictionary(c => c.Name, c => c.Cid, StringComparer.OrdinalIgnoreCase); + sqliteTable.Columns = sqliteTable.Columns.OrderBy(c => columnOrder[c.Name]).ToList(); + } + sqliteTable.ColumnMappings = sqliteTable.Columns .Select(x => new MappingInfo @@ -889,7 +902,12 @@ private void RecreateTableCore(SQLiteTableInfo sqliteTableInfo) var targetIntermediateTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}{IntermediateTableSuffix}"); var targetTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}"); - var columnDbFields = sqliteTableInfo.Columns.Cast(); + // Catalog columns still expose legacy membership flags during the v13 transition. + // The table constraint is authoritative; clear flags only on private copies. + var columns = sqliteTableInfo.Columns.Select(c => c.CopyDefinition()).ToArray(); + if (sqliteTableInfo.PrimaryKey != null) + foreach (var column in columns) column.ColumnProperty &= ~ColumnProperty.PrimaryKey; + var columnDbFields = columns.Cast(); var foreignKeyDbFields = sqliteTableInfo.ForeignKeys.Cast(); var indexDbFields = sqliteTableInfo.Indexes.Cast(); var uniqueDbFields = sqliteTableInfo.Uniques.Cast(); @@ -898,6 +916,7 @@ private void RecreateTableCore(SQLiteTableInfo sqliteTableInfo) var dbFields = columnDbFields.Concat(foreignKeyDbFields) .Concat(uniqueDbFields) .Concat(checkConstraintDbFields) + .Concat(sqliteTableInfo.PrimaryKey == null ? Array.Empty() : new IDbField[] { sqliteTableInfo.PrimaryKey }) .ToArray(); // ToHashSet() not available in older .NET versions so we create it old-fashioned. @@ -1075,11 +1094,8 @@ public override void ChangeColumn(string table, Column column) throw new Exception("Column does not exists."); } - sqliteInfo.Columns = sqliteInfo.Columns - .Where(x => !x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - sqliteInfo.Columns.Add(column); + var columnIndex = sqliteInfo.Columns.FindIndex(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); + sqliteInfo.Columns[columnIndex] = column.CopyDefinition(); RecreateTable(sqliteInfo); } @@ -1693,6 +1709,7 @@ public override void RemovePrimaryKey(string tableName) } var sqliteInfoTable = GetSQLiteTableInfo(tableName); + sqliteInfoTable.PrimaryKey = null; foreach (var column in sqliteInfoTable.Columns) {