From 281628047df688a5d46c223a2076f12bc89ef2f3 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 14:52:28 +0200 Subject: [PATCH 01/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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) { From 811a85218148bc6b6d35fd743794fab89171bf4b Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 18:48:56 +0200 Subject: [PATCH 21/34] Replace column constraint flags with explicit v13 schema definitions Remove ColumnProperty, flag-taking constructors and AddColumn overloads, column primary-key membership properties, and the obsolete SchemaBuilder API. Columns expose independent nullability, identity, unsigned and explicit collation attributes; table keys and unique/check constraints remain named ordered objects. Consolidate authoring on MigrationBuilder and update examples and the operation coverage inventory. Render column clauses through ColumnAttribute dialect mappings. Stop implicit unique-constraint ownership and removal during ChangeColumn, remove SQL Server collation inference, and change Oracle columns in place. SQLite preserves named and unnamed constraints independently of column attributes, reports physical column order, distinguishes AUTOINCREMENT, and preserves table uniqueness when removing indexes. Add structured constraint readers for Db2, Firebird, Informix and Sybase for live matrix validation. Document the source/API breaks and concrete replacements in the 12.1-to-13 guide. Convert legacy flag tests to schema and persisted-data assertions. Local validation: complete solution builds; 87 unit and 192 SQLite tests pass with no skips. Live provider validation is required on the stacked PR. --- README.md | 6 +- docs/fluent-operation-coverage.json | 1 - docs/index.html | 3 +- docs/migration-guide-12.1-to-13.md | 109 +++++-- docs/runner-guide.md | 2 +- examples/FluentQuickStart/Program.cs | 2 +- .../ColumnPropertyMapperTest.cs | 53 +--- src/Migrator.Tests/FluentOperationsTests.cs | 23 +- .../ColumnPropertyExtensionTests.cs | 98 ++----- ...leTransformationProviderExtensionsTests.cs | 6 +- src/Migrator.Tests/ProviderCorrectionTests.cs | 17 +- src/Migrator.Tests/ProviderDefinitionTests.cs | 4 +- .../Base/TransformationProviderSimpleBase.cs | 25 +- .../Generic/Generic_AddPrimaryKey.cs | 13 +- .../Generic/Generic_AddTableTestsBase.cs | 46 ++- .../Generic/Generic_ChangeColumnTestsBase.cs | 20 +- .../Generic/Generic_ConstraintExistsBase.cs | 8 +- .../Generic/Generic_DefaultValueTestsBase.cs | 15 +- .../Generic/Generic_GetColumnsTestsBase.cs | 16 +- ...Generic_UpdateFromTableToTableTestsBase.cs | 8 +- ...mationProviderGenericMiscConstraintBase.cs | 28 +- .../TransformationProviderGenericMiscTests.cs | 18 +- .../Providers/Live/LiveDatabaseTests.cs | 26 +- .../Live/LiveMetadataRegressionTests.cs | 24 +- ...racleTransformationProviderGenericTests.cs | 6 +- ...leTransformationProvider_AddColumnTests.cs | 9 +- ...TransformationProvider_GetColumns_Tests.cs | 4 +- ...TransformationProvider_TableExistsTests.cs | 2 +- ...SQLTransformationProvider_AddIndexTests.cs | 15 +- ...ransformationProvider_ChangeColumnTests.cs | 18 +- ...ansformationProvider_MetadataScopeTests.cs | 4 +- ...ionProvider_PrimaryKeyWithIdentityTests.cs | 5 +- ...ansformationProvider_ReservedWordsTests.cs | 5 +- ...verTransformationProvider_AddTableTests.cs | 9 +- ...ransformationProvider_ChangeColumnTests.cs | 53 ++-- ...rTransformationProvider_GetColumnsTests.cs | 4 +- .../SqlServerTransformationProviderTests.cs | 6 +- .../SQLiteTransformationProviderTests.cs | 47 ++- ...teTransformationProvider_AddColumnTests.cs | 32 ++- ...ansformationProvider_AddForeignKeyTests.cs | 19 +- ...ansformationProvider_AddPrimaryKeyTests.cs | 13 +- ...iteTransformationProvider_AddTableTests.cs | 54 ++-- ...ransformationProvider_ChangeColumnTests.cs | 33 ++- ...mationProvider_GetCheckConstraintsTests.cs | 2 +- ...eTransformationProvider_GetColumnsTests.cs | 31 +- ...nsformationProvider_GetForeignKeysTests.cs | 13 +- ...onProvider_GetPragmaTableInfoItemsTests.cs | 2 +- ...SQLiteTransformationProvider_GetUniques.cs | 4 +- ...ionProvider_PropertyColumnIdentityTests.cs | 8 +- ...iteTransformationProvider_RecreateTable.cs | 8 +- ...ationProvider_RemoveAllConstraintsTests.cs | 19 +- ...ransformationProvider_RemoveColumnTests.cs | 68 +++-- ...ransformationProvider_RenameColumnTests.cs | 15 +- src/Migrator.Tests/SchemaBuilderTests.cs | 105 ++++--- src/Migrator.Tests/SchemaConstraintTests.cs | 6 +- src/Migrator.Tests/ScriptTests.cs | 76 ++--- .../Settings/ConfigurationReader.cs | 140 ++++----- ...ngTableTransformationProviderExtensions.cs | 4 +- src/Migrator/Framework/Column.cs | 83 +----- src/Migrator/Framework/ColumnAttribute.cs | 10 + src/Migrator/Framework/ColumnProperty.cs | 71 ----- .../Framework/ColumnPropertyExtensions.cs | 24 -- .../Framework/Fluent/MigrationBuilder.cs | 25 +- src/Migrator/Framework/Fluent/Operations.cs | 8 +- src/Migrator/Framework/IColumn.cs | 10 +- src/Migrator/Framework/IDialect.cs | 6 +- .../Framework/ITransformationProvider.cs | 66 ----- src/Migrator/Framework/Index.cs | 2 +- .../SchemaBuilder/AddColumnExpression.cs | 39 --- .../SchemaBuilder/AddTableExpression.cs | 36 --- .../SchemaBuilder/DeleteTableExpression.cs | 29 -- .../Framework/SchemaBuilder/FluentColumn.cs | 81 ------ .../Framework/SchemaBuilder/ForeignKey.cs | 27 -- .../Framework/SchemaBuilder/IColumnOptions.cs | 12 - .../SchemaBuilder/IDeleteTableOptions.cs | 23 -- .../Framework/SchemaBuilder/IFluentColumn.cs | 21 -- .../SchemaBuilder/IForeignKeyOptions.cs | 19 -- .../SchemaBuilder/ISchemaBuilderExpression.cs | 19 -- .../SchemaBuilder/RenameTableExpression.cs | 31 -- .../Framework/SchemaBuilder/SchemaBuilder.cs | 190 ------------ .../Providers/ColumnPropertiesMapper.cs | 262 +++-------------- .../Providers/ConstraintMetadataReader.cs | 35 ++- src/Migrator/Providers/Dialect.cs | 19 +- src/Migrator/Providers/Impl/DB2/DB2Dialect.cs | 11 +- .../Impl/DB2/DB2TransformationProvider.cs | 14 +- .../FirebirdColumnPropertiesMapper.cs | 8 +- .../Impl/Firebird/FirebirdDialect.cs | 4 +- .../FirebirdTransformationProvider.cs | 14 +- .../Impl/Informix/InformixDialect.cs | 9 +- .../InformixTransformationProvider.cs | 37 ++- .../Providers/Impl/Ingres/IngresDialect.cs | 4 +- .../Impl/Mysql/MySqlTransformationProvider.cs | 5 +- .../Providers/Impl/Mysql/MysqlDialect.cs | 7 +- .../Oracle/OracleColumnPropertiesMapper.cs | 6 +- .../Providers/Impl/Oracle/OracleDialect.cs | 6 +- .../Oracle/OracleTransformationProvider.cs | 107 ++----- .../Impl/PostgreSQL/PostgreSQLDialect.cs | 10 +- .../PostgreSQLTransformationProvider.cs | 17 +- .../SQLite/SQLiteColumnPropertiesMapper.cs | 36 +-- .../Providers/Impl/SQLite/SQLiteDialect.cs | 5 +- .../SQLite/SQLiteTransformationProvider.cs | 272 ++++-------------- .../Impl/SqlServer/SqlServerDialect.cs | 6 +- .../SqlServerTransformationProvider.cs | 85 +----- .../Providers/Impl/Sybase/SybaseDialect.cs | 9 +- .../Sybase/SybaseTransformationProvider.cs | 36 ++- .../Providers/NoOpTransformationProvider.cs | 37 +-- .../Providers/TransformationProvider.cs | 240 ++-------------- 107 files changed, 1011 insertions(+), 2532 deletions(-) create mode 100644 src/Migrator/Framework/ColumnAttribute.cs delete mode 100644 src/Migrator/Framework/ColumnProperty.cs delete mode 100644 src/Migrator/Framework/ColumnPropertyExtensions.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/AddColumnExpression.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/AddTableExpression.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/DeleteTableExpression.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/FluentColumn.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/ForeignKey.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/IColumnOptions.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/IDeleteTableOptions.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/IFluentColumn.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/IForeignKeyOptions.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/ISchemaBuilderExpression.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/RenameTableExpression.cs delete mode 100644 src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs diff --git a/README.md b/README.md index ac0fa223..2470d152 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ public class CreateUsers : Migration public override void Up() { Database.AddTable("Users", - new Column("Id", DbType.Int32, ColumnProperty.NotNull), + new Column("Id", DbType.Int32) { IsNullable = false }, new Column("Name", DbType.String, 255)); Database.AddPrimaryKey("PK_Users", "Users", "Id"); } @@ -315,3 +315,7 @@ This project continues the original [Migrator.NET](https://github.com/migratordo ## 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. + +### Version 13 source changes + +The unreleased v13 stack separates columns from named table constraints and removes the old column flags and duplicate fluent builder. See the [12.1-to-13 migration guide](docs/migration-guide-12.1-to-13.md) before recompiling migrations. These source features are not claims about the published 12.1 NuGet package. diff --git a/docs/fluent-operation-coverage.json b/docs/fluent-operation-coverage.json index 11022870..af329b52 100644 --- a/docs/fluent-operation-coverage.json +++ b/docs/fluent-operation-coverage.json @@ -69,6 +69,5 @@ "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))", "GetTableConstraints": "Schema.Table(table).ConstraintDefinitions()" } diff --git a/docs/index.html b/docs/index.html index 09171875..2a19fffe 100644 --- a/docs/index.html +++ b/docs/index.html @@ -200,8 +200,7 @@

Describe the change

public override void Up() { Database.AddTable("Users", - new Column("Id", DbType.Int32, - ColumnProperty.NotNull), + new Column("Id", DbType.Int32) { IsNullable = false }, new Column("Name", DbType.String, 255)); Database.AddPrimaryKey("PK_Users", "Users", "Id"); } diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index 4db5ad0f..caf8e090 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -2,16 +2,88 @@ 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) +## Schema model 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. +The v13 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. +The old column flags and duplicate fluent builder are removed. These are source-breaking changes: update historical migration source before recompiling for v13; the runner preserves the existing history table format. ## Implemented breaking changes +### Column flags, constructors and inspection + +`ColumnProperty`, its extensions, `Column.ColumnProperty`, +`IColumn.ColumnProperty`, `IsPrimaryKey` and `IsPrimaryKeyNonClustered` are +removed. Constructors and `AddColumn` overloads taking flags are removed. + +| 12.1 | 13 | +| --- | --- | +| `ColumnProperty.Null` / `None` | `IsNullable = true` (the default) | +| `ColumnProperty.NotNull` | `IsNullable = false` | +| `ColumnProperty.Identity` | `IsIdentity = true` | +| `ColumnProperty.Unsigned` | `IsUnsigned = true` | +| `ColumnProperty.PrimaryKey` | `new PrimaryKeyConstraint(name, columns)` in the table definition | +| `PrimaryKeyWithIdentity` | Identity on the column plus a separate primary key | +| `PrimaryKeyNonClustered` | `new PrimaryKeyConstraint(name, columns) { NonClustered = true }` | +| `ColumnProperty.Unique` | `new UniqueConstraint(name, columns)` | +| `ColumnProperty.Indexed` | An explicit `Index` definition | +| `ColumnProperty.CaseSensitive` | `Collation = "provider_collation_name"` | + +For example, replace a flagged `AddColumn` call with: + +```csharp +Database.AddColumn("Users", new Column("Email", DbType.String, 200) +{ + IsNullable = false, + DefaultValue = "unknown" +}); +Database.AddUniqueConstraint("UQ_Users_Email", "Users", "Email"); +``` + +Use `DefaultValue = 10` for numeric defaults: a positional integer after the +type is the column **size**, not its default. Unsupported unsigned/collation +combinations produce diagnostics. Collation names are explicit; the SQL Server +provider no longer queries or guesses a case-sensitive database collation. +SQLite's former `CaseSensitive` flag emitted `NOCASE`; choose `BINARY` or +`NOCASE` explicitly for the desired behavior. + +Read primary/unique membership through `GetTableConstraints(table)`, retaining +the constraint's member order. `GetColumns` returns column attributes only. +SQLite column metadata now follows physical column order, not primary-key order. +A plain SQLite INTEGER primary key remains a rowid alias; `IsIdentity` indicates +explicit `AUTOINCREMENT`, which is preserved on rebuild. + +### One fluent authoring API + +The obsolete `Framework.SchemaBuilder` namespace and `ExecuteSchemaBuilder` +method are removed. Use `Framework.Fluent.MigrationBuilder` and +`builder.Apply(provider)`, or derive from `FluentMigration`. +Replace `AddTable/AddColumn` chains with `Create.Table(...).WithColumn(...)`. +Replace `WithProperty`, unnamed `PrimaryKey()` and `Unique()` with +`NotNullable()`, `Identity()`, `Unsigned()`, `WithCollation(name)`, +`WithPrimaryKey(name, columns)` and `WithUniqueConstraint(name, columns)`. +Use `Create.ForeignKey` with separate delete/update actions. + +### Column changes do not own constraints + +`ChangeColumn` changes attributes without inferring creation or removal of +unique constraints. SQL Server's `AdoptColumnUniqueConstraint` and the implicit +ownership marker mechanism are removed. Use explicit `AddUniqueConstraint` and +`RemoveConstraint` calls. Existing extended-property markers are harmless; +v13 does not use them to delete constraints. + +Oracle changes columns in place so native constraints remain attached; a type +conversion that Oracle cannot perform must be expressed as an explicit data +migration. Identity validation runs before table creation, and identity no longer +requires primary-key membership. + +SQLite `RemoveAllIndexes` now preserves table UNIQUE constraints. To remove +constraints too, call `RemoveAllConstraints` explicitly. SQLite +`PrimaryKeyExists(table, name)` checks the actual name (use null for an unnamed +legacy key), rather than returning true for any primary key. + ### `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). @@ -28,7 +100,7 @@ Database.AddTable("Users", 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. +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. 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. @@ -36,11 +108,23 @@ Fluent equivalent: append `.WithPrimaryKey("PK_Users", "TenantId", "Id")`, `.Wit 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. +Structured readers cover SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Db2, Firebird, Informix and Sybase. Each reader is exercised in its live CI job; unsupported engines throw `NotSupportedException`. 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. +`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` replaces `RegisterProperty/SqlForProperty` with `RegisterColumnAttribute/SqlForColumnAttribute`, using the non-flag `ColumnAttribute` enum (Null, NotNull, Identity, Unsigned). Custom column mappers use explicit column attributes; removed helpers include `IndexSql`, `PropertySelected`, `AddPrimaryKey`, `AddUnique`, and `AddForeignKey`. `GetPrimaryKeys(IEnumerable)` and column/index joining helpers are removed: inspect table constraints and create indexes explicitly. `GetCollationSql` generates a supported collation clause or throws before DDL. `IDialect` also 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. + +### 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. ## Provider authors and dialects (design) @@ -59,16 +143,3 @@ 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/docs/runner-guide.md b/docs/runner-guide.md index 8689ef43..ade59538 100644 --- a/docs/runner-guide.md +++ b/docs/runner-guide.md @@ -17,7 +17,7 @@ public class CreateUsers : AutoReversingMigration public override void BuildUp(MigrationBuilder migration) { migration.Create.Table("Users") - .WithColumn("Id").AsInt32().PrimaryKey() + .WithColumn("Id").AsInt32().WithPrimaryKey("PK_Id", "Id") .WithColumn("Name").AsString(255).NotNullable(); } } diff --git a/examples/FluentQuickStart/Program.cs b/examples/FluentQuickStart/Program.cs index b9e2ba48..16301a5e 100644 --- a/examples/FluentQuickStart/Program.cs +++ b/examples/FluentQuickStart/Program.cs @@ -24,7 +24,7 @@ public class CreateUsers : AutoReversingMigration public override void BuildUp(MigrationBuilder migration) { migration.Create.Table("Users") - .WithColumn("Id").AsInt32().PrimaryKey() + .WithColumn("Id").AsInt32().WithPrimaryKey("PK_Id", "Id") .WithColumn("Name").AsString(255).NotNullable(); } } diff --git a/src/Migrator.Tests/ColumnPropertyMapperTest.cs b/src/Migrator.Tests/ColumnPropertyMapperTest.cs index 3add18d0..e9794567 100644 --- a/src/Migrator.Tests/ColumnPropertyMapperTest.cs +++ b/src/Migrator.Tests/ColumnPropertyMapperTest.cs @@ -16,7 +16,7 @@ public class ColumnPropertyMapperTest public void OracleCreatesNotNullSql() { var mapper = new ColumnPropertiesMapper(new OracleDialect(), "varchar(30)"); - mapper.MapColumnProperties(new Column("foo", DbType.String, ColumnProperty.NotNull)); + mapper.MapColumnProperties(new Column("foo",DbType.String){IsNullable = false}); Assert.That("foo varchar(30) NOT NULL", Is.EqualTo(mapper.ColumnSql)); } @@ -25,46 +25,18 @@ public void OracleCreatesSql() { var mapper = new ColumnPropertiesMapper(new OracleDialect(), "varchar(30)"); mapper.MapColumnProperties(new Column("foo", DbType.String, 0)); - Assert.That("foo varchar(30)", Is.EqualTo(mapper.ColumnSql)); + Assert.That("foo varchar(30) NULL", Is.EqualTo(mapper.ColumnSql)); } - [Test] - public void OracleIndexSqlIsNoNullWhenIndexed() - { - var mapper = new ColumnPropertiesMapper(new OracleDialect(), "char(1)"); - mapper.MapColumnProperties(new Column("foo", DbType.StringFixedLength, 1, ColumnProperty.Indexed)); - Assert.That(mapper.IndexSql, Is.Not.Null); - } - [Test] - public void OracleIndexSqlIsNullWhenIndexedFalse() - { - var mapper = new ColumnPropertiesMapper(new OracleDialect(), "char(1)"); - mapper.MapColumnProperties(new Column("foo", DbType.StringFixedLength, 1, 0)); - Assert.That(mapper.IndexSql, Is.Null); - } - [Test] - public void PostgresIndexSqlIsNoNullWhenIndexed() - { - var mapper = new ColumnPropertiesMapper(new PostgreSQLDialect(), "char(1)"); - mapper.MapColumnProperties(new Column("foo", DbType.StringFixedLength, 1, ColumnProperty.Indexed)); - Assert.That(mapper.IndexSql, Is.Not.Null); - } - [Test] - public void PostgresIndexSqlIsNullWhenIndexedFalse() - { - var mapper = new ColumnPropertiesMapper(new PostgreSQLDialect(), "char(1)"); - mapper.MapColumnProperties(new Column("foo", DbType.StringFixedLength, 1, 0)); - Assert.That(mapper.IndexSql, Is.Null); - } [Test] public void SqlServerCreatesNotNullSql() { var mapper = new ColumnPropertiesMapper(new SqlServerDialect(), "varchar(30)"); - mapper.MapColumnProperties(new Column("foo", DbType.String, ColumnProperty.NotNull)); + mapper.MapColumnProperties(new Column("foo",DbType.String){IsNullable = false}); Assert.That("[foo] varchar(30) NOT NULL", Is.EqualTo(mapper.ColumnSql)); } @@ -73,10 +45,10 @@ public void SqlServerCreatesSqWithBooleanDefault() { var mapper = new ColumnPropertiesMapper(new SqlServerDialect(), "bit"); mapper.MapColumnProperties(new Column("foo", DbType.Boolean, 0, false)); - Assert.That("[foo] bit DEFAULT 0", Is.EqualTo(mapper.ColumnSql)); + Assert.That("[foo] bit NULL DEFAULT 0", Is.EqualTo(mapper.ColumnSql)); mapper.MapColumnProperties(new Column("bar", DbType.Boolean, 0, true)); - Assert.That("[bar] bit DEFAULT 1", Is.EqualTo(mapper.ColumnSql)); + Assert.That("[bar] bit NULL DEFAULT 1", Is.EqualTo(mapper.ColumnSql)); } [Test] @@ -84,7 +56,7 @@ public void SqlServerCreatesSqWithDefault() { var mapper = new ColumnPropertiesMapper(new SqlServerDialect(), "varchar(30)"); mapper.MapColumnProperties(new Column("foo", DbType.String, 0, "'NEW'")); - Assert.That("[foo] varchar(30) DEFAULT '''NEW'''", Is.EqualTo(mapper.ColumnSql)); + Assert.That("[foo] varchar(30) NULL DEFAULT '''NEW'''", Is.EqualTo(mapper.ColumnSql)); } [Test] @@ -92,7 +64,7 @@ public void SqlServerCreatesSqWithNullDefault() { var mapper = new ColumnPropertiesMapper(new SqlServerDialect(), "varchar(30)"); mapper.MapColumnProperties(new Column("foo", DbType.String, 0, "NULL")); - Assert.That("[foo] varchar(30) DEFAULT 'NULL'", Is.EqualTo(mapper.ColumnSql)); + Assert.That("[foo] varchar(30) NULL DEFAULT 'NULL'", Is.EqualTo(mapper.ColumnSql)); } [Test] @@ -100,22 +72,15 @@ public void SqlServerCreatesSql() { var mapper = new ColumnPropertiesMapper(new SqlServerDialect(), "varchar(30)"); mapper.MapColumnProperties(new Column("foo", DbType.String, 0)); - Assert.That("[foo] varchar(30)", Is.EqualTo(mapper.ColumnSql)); + Assert.That("[foo] varchar(30) NULL", Is.EqualTo(mapper.ColumnSql)); } - [Test] - public void SqlServerIndexSqlIsNoNullWhenIndexed() - { - var mapper = new ColumnPropertiesMapper(new SqlServerDialect(), "char(1)"); - mapper.MapColumnProperties(new Column("foo", DbType.StringFixedLength, 1, ColumnProperty.Indexed)); - Assert.That(mapper.IndexSql, Is.Null); - } [Test] public void SQLiteIndexSqlWithEmptyStringDefault() { var mapper = new ColumnPropertiesMapper(new SQLiteDialect(), "varchar(30)"); - mapper.MapColumnProperties(new Column("foo", DbType.String, 1, ColumnProperty.NotNull, string.Empty)); + mapper.MapColumnProperties(new Column("foo",DbType.String,1,string.Empty){IsNullable = false}); Assert.That("foo varchar(30) NOT NULL DEFAULT ''", Is.EqualTo(mapper.ColumnSql)); } } \ No newline at end of file diff --git a/src/Migrator.Tests/FluentOperationsTests.cs b/src/Migrator.Tests/FluentOperationsTests.cs index 11350894..94e2f626 100644 --- a/src/Migrator.Tests/FluentOperationsTests.cs +++ b/src/Migrator.Tests/FluentOperationsTests.cs @@ -48,7 +48,7 @@ public void TransactionIncompatibilityIsRejectedBeforeEarlierOperationsRun() } [Test] public void TableIsOneCompleteOperationAndDoesNotMutateInput() { - var column = new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey); + var column = new Column("Id",DbType.Int32){IsNullable = false}; var builder = new MigrationBuilder(); builder.Create.Table("Example").WithFields(column); column.Name = "Changed"; var operation = (CreateTableOperation)builder.Build().Single(); @@ -154,7 +154,7 @@ [Test] public void CallbacksCannotBePreviewedOrAutomaticallyReversed() } [Test, Category("SQLite")] public void FluentAndPreviewProduceEquivalentDataAndAutomaticDownRemovesTable() { - var builder = new MigrationBuilder(); builder.Create.Table("Example").WithColumn("Id").AsInt32().PrimaryKey().WithColumn("Name").AsString(); + var builder = new MigrationBuilder(); builder.Create.Table("Example").WithColumn("Id").AsInt32().WithPrimaryKey("PK_Id", "Id").WithColumn("Name").AsString(); builder.Insert.IntoTable("Example").Row(new[] { "Id", "Name" }, new object[] { 1, "O'Brien" }); using var connection = new SqliteConnection("Data Source=:memory:"); connection.Open(); using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); @@ -166,24 +166,25 @@ [Test] public void CallbacksCannotBePreviewedOrAutomaticallyReversed() builder.Build()[0].Reverse().Apply(provider); Assert.That(provider.TableExists("Example"), Is.False); } - [Test, Category("SQLite")] public void LegacyBuilderRetainsForeignKeyAction() + [Test, Category("SQLite")] public void BuilderRetainsForeignKeyAction() { using var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True"); connection.Open(); using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); - provider.AddTable("Parent", new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey)); - var builder = new DotNetProjects.Migrator.Framework.SchemaBuilder.SchemaBuilder(); - builder.AddTable("Child").AddColumn("ParentId").OfType(DbType.Int32).AsForeignKey().ReferencedTo("Parent", "Id").WithConstraint(ForeignKeyConstraintType.Cascade); - provider.ExecuteSchemaBuilder(builder); + provider.AddTable("Parent", new Column("Id",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "Parent", "Id")); + var builder = new MigrationBuilder(); + builder.Create.Table("Child").WithColumn("ParentId").AsInt32(); + builder.Create.ForeignKey("FK_Child", "Child", new[] { "ParentId" }, "Parent", new[] { "Id" }, ForeignKeyConstraintType.Cascade); + builder.Apply(provider); provider.ExecuteNonQuery("INSERT INTO Parent VALUES (1); INSERT INTO Child VALUES (1); DELETE FROM Parent WHERE Id=1"); Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT COUNT(*) FROM Child")), Is.Zero); } - [Test, Category("SQLite")] public void LegacyBuilderCreatesCompleteTable() + [Test, Category("SQLite")] public void BuilderCreatesCompleteTable() { using var connection = new SqliteConnection("Data Source=:memory:"); connection.Open(); using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); - var builder = new DotNetProjects.Migrator.Framework.SchemaBuilder.SchemaBuilder(); - builder.AddTable("Example").AddColumn("Id").OfType(DbType.Int32); - provider.ExecuteSchemaBuilder(builder); + var builder = new MigrationBuilder(); + builder.Create.Table("Example").WithColumn("Id").AsInt32(); + builder.Apply(provider); Assert.That(provider.ColumnExists("Example", "Id"), Is.True); } } diff --git a/src/Migrator.Tests/Framework/ColumnProperties/ColumnPropertyExtensionTests.cs b/src/Migrator.Tests/Framework/ColumnProperties/ColumnPropertyExtensionTests.cs index d2f822d6..d93fe09f 100644 --- a/src/Migrator.Tests/Framework/ColumnProperties/ColumnPropertyExtensionTests.cs +++ b/src/Migrator.Tests/Framework/ColumnProperties/ColumnPropertyExtensionTests.cs @@ -1,90 +1,34 @@ -using NUnit.Framework; -using DotNetProjects.Migrator.Framework; using System; using System.Linq; - +using DotNetProjects.Migrator.Framework; +using NUnit.Framework; namespace Migrator.Tests.Framework.ColumnProperties; - -public class ColumnPropertyExtensionsTests +public class ColumnModelTests { [Test] - public void Clear() + public void ColumnsDoNotExposeConstraintFlags() { - // Arrange - var columnProperty = ColumnProperty.PrimaryKey | ColumnProperty.NotNull; - - // Act - columnProperty = columnProperty.Clear(ColumnProperty.PrimaryKey); - - // Assert - Assert.That(columnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); + Assert.That(typeof(Column).Assembly.GetType("DotNetProjects.Migrator.Framework.ColumnProperty"), Is.Null); + Assert.That(typeof(Column).GetProperties().Select(p => p.Name), Does.Not.Contain("IsPrimaryKey")); + Assert.That(typeof(Column).GetProperties().Select(p => p.Name), Does.Not.Contain("ColumnProperty")); + Assert.That(Enum.GetNames(), Is.EquivalentTo(new[] { "Null", "NotNull", "Identity", "Unsigned" })); } - [Test] - public void IsSet() + public void ColumnAttributesAreIndependent() { - // Arrange - var columnProperty = ColumnProperty.PrimaryKeyWithIdentity | ColumnProperty.NotNull; - - // Act - var actualData = GetAllSingleColumnProperties().Select(x => new - { - ColumnPropertyString = x.ToString(), - IsSet = columnProperty.IsSet(x), - IsNotSet = columnProperty.IsNotSet(x) - }) - .ToList(); - - // Assert - string[] expectedSet = [nameof(ColumnProperty.PrimaryKey), nameof(ColumnProperty.NotNull), nameof(ColumnProperty.Identity)]; - var actualDataShouldBeTrue = actualData.Where(x => expectedSet.Any(y => y == x.ColumnPropertyString)).ToList(); - var actualDataShouldBeFalse = actualData.Where(x => !expectedSet.Any(y => y == x.ColumnPropertyString)).ToList(); - - Assert.That(actualDataShouldBeTrue.Select(x => x.IsSet), Has.All.True); - Assert.That(actualDataShouldBeFalse.Select(x => x.IsSet), Has.All.False); + var column = new Column("Id") { IsIdentity = true, IsNullable = true, IsUnsigned = true }; + Assert.That(column.IsNullable, Is.True); + Assert.That(column.IsIdentity, Is.True); + column.IsNullable = false; + Assert.That(column.IsIdentity, Is.True); + Assert.That(column.IsUnsigned, Is.True); } - [Test] - public void IsNotSet() - { - // Arrange - var columnProperty = ColumnProperty.PrimaryKeyWithIdentity | ColumnProperty.NotNull; - - // Act - var actualData = GetAllSingleColumnProperties().Select(x => new - { - ColumnPropertyString = x.ToString(), - IsSet = columnProperty.IsNotSet(x), - IsNotSet = columnProperty.IsNotSet(x) - }) - .ToList(); - - // Assert - string[] expectedSet = [nameof(ColumnProperty.PrimaryKey), nameof(ColumnProperty.NotNull), nameof(ColumnProperty.Identity)]; - var actualDataShouldBeFalse = actualData.Where(x => expectedSet.Any(y => y == x.ColumnPropertyString)).ToList(); - var actualDataShouldBeTrue = actualData.Where(x => !expectedSet.Any(y => y == x.ColumnPropertyString)).ToList(); - - Assert.That(actualDataShouldBeTrue.Select(x => x.IsNotSet), Has.All.True); - Assert.That(actualDataShouldBeFalse.Select(x => x.IsNotSet), Has.All.False); - } - - [Test] - public void Set_Success() - { - // Arrange - var columnProperty = ColumnProperty.NotNull; - - // Act - var result = columnProperty.Set(ColumnProperty.PrimaryKeyWithIdentity); - - // Assert - var expected = ColumnProperty.NotNull | ColumnProperty.PrimaryKeyWithIdentity; - - Assert.That(result, Is.EqualTo(expected)); - } - - private ColumnProperty[] GetAllSingleColumnProperties() + public void KeyDefinitionsCopyOrderedCallerArrays() { - return [.. Enum.GetValues().Where(x => x == 0 || (x & (x - 1)) == 0)]; + var columns = new[] { "Second", "First" }; + var key = new PrimaryKeyConstraint("PK", columns); + columns[0] = "Changed"; + Assert.That(key.KeyColumns, Is.EqualTo(new[] { "Second", "First" })); } -} \ No newline at end of file +} diff --git a/src/Migrator.Tests/JoiningTableTransformationProviderExtensionsTests.cs b/src/Migrator.Tests/JoiningTableTransformationProviderExtensionsTests.cs index dfdaabff..5eee5d79 100644 --- a/src/Migrator.Tests/JoiningTableTransformationProviderExtensionsTests.cs +++ b/src/Migrator.Tests/JoiningTableTransformationProviderExtensionsTests.cs @@ -51,7 +51,7 @@ public void AddManyToManyJoiningTable_CreatesLeftHandSideColumn_WithCorrectName( Assert.That(lhsColumn.Name, Is.EqualTo("TestScenarioId")); Assert.That(lhsColumn.Type, Is.EqualTo(DbType.Guid)); - Assert.That(ColumnProperty.NotNull, Is.EqualTo(lhsColumn.ColumnProperty)); + Assert.That(lhsColumn.IsNullable, Is.False); }); _provider.AddManyToManyJoiningTable("dbo", "TestScenarios", "Id", "Versions", "Id"); @@ -102,7 +102,7 @@ public void AddManyToManyJoiningTable_CreatesRightHandSideColumn_WithCorrectName Assert.That(rhsColumn.Name, Is.EqualTo("VersionId")); Assert.That(DbType.Guid, Is.EqualTo(rhsColumn.Type)); - Assert.That(ColumnProperty.NotNull, Is.EqualTo(rhsColumn.ColumnProperty)); + Assert.That(rhsColumn.IsNullable, Is.False); }); _provider.AddManyToManyJoiningTable("dbo", "TestScenarios", "Id", "Versions", "Id"); @@ -119,7 +119,7 @@ public void AddManyToManyJoiningTable_CreatesRightHandSideForeignKey_WithCorrect Assert.That(rhsColumn.Name, Is.EqualTo("VersionId")); Assert.That(DbType.Guid, Is.EqualTo(rhsColumn.Type)); - Assert.That(ColumnProperty.NotNull, Is.EqualTo(rhsColumn.ColumnProperty)); + Assert.That(rhsColumn.IsNullable, Is.False); Assert.That(callInfo[1] as string, Is.EqualTo("dbo.TestScenarioVersions")); Assert.That(callInfo[2] as string, Is.EqualTo("VersionId")); diff --git a/src/Migrator.Tests/ProviderCorrectionTests.cs b/src/Migrator.Tests/ProviderCorrectionTests.cs index 424a525a..bd4e105c 100644 --- a/src/Migrator.Tests/ProviderCorrectionTests.cs +++ b/src/Migrator.Tests/ProviderCorrectionTests.cs @@ -40,15 +40,16 @@ [Test] public void TableCreationRetainsCallerPrimaryKeyDefinitions() { using var connection = new SqliteConnection("Data Source=:memory:"); connection.Open(); using var provider = ProviderFactory.Create(ProviderTypes.SQLite, connection, null); - var first = new Column("First", DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.Null); - var second = new Column("Second", DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.Null); - provider.AddTable("Composite", first, second); - Assert.That(first.ColumnProperty, Is.EqualTo(ColumnProperty.PrimaryKey | ColumnProperty.Null)); - Assert.That(second.ColumnProperty, Is.EqualTo(ColumnProperty.PrimaryKey | ColumnProperty.Null)); - provider.Insert("Composite", new[] { "First", "Second" }, new object[] { 1, null }); + var first = new Column("First",DbType.Int32); + var second = new Column("Second",DbType.Int32); + provider.AddTable("Composite", first, second, new PrimaryKeyConstraint("PK_Composite", "Second", "First")); + Assert.That(first.IsNullable, Is.True); + Assert.That(second.IsNullable, Is.True); + Assert.Catch(() => provider.Insert("Composite", new[] { "First", "Second" }, new object[] { 1, null })); + provider.Insert("Composite", new[] { "First", "Second" }, new object[] { 1, 2 }); Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT COUNT(*) FROM Composite")), Is.EqualTo(1)); - provider.AddTable("Reused", first, second); - Assert.That(provider.GetColumns("Reused").Count(c => c.IsPrimaryKey), Is.EqualTo(2)); + provider.AddTable("Reused", first, second, new PrimaryKeyConstraint("PK_Reused", "Second", "First")); + Assert.That(provider.GetTableConstraints("Reused").OfType().Single().KeyColumns.Length, Is.EqualTo(2)); } [Test] public void RebuildPreservesTriggerAndUpdateAction() { diff --git a/src/Migrator.Tests/ProviderDefinitionTests.cs b/src/Migrator.Tests/ProviderDefinitionTests.cs index fe580d23..f29e771a 100644 --- a/src/Migrator.Tests/ProviderDefinitionTests.cs +++ b/src/Migrator.Tests/ProviderDefinitionTests.cs @@ -37,9 +37,9 @@ [Test] public void NullableScalarRetainsTypedValuesAndHandlesNulls() [Test] public void ChangeColumnDoesNotClearCallerUniqueFlag() { using var provider = new RecordingProvider(); - var column = new Column("Value", DbType.Int32, ColumnProperty.Unique | ColumnProperty.NotNull); + var column = new Column("Value",DbType.Int32){IsNullable = false}; provider.ChangeColumn("Example", column); - Assert.That(column.ColumnProperty, Is.EqualTo(ColumnProperty.Unique | ColumnProperty.NotNull)); + Assert.That(column.IsNullable, Is.False); } [Test] public void QuotingReturnsNewArrayWithoutChangingCallerNames() { diff --git a/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs b/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs index 94050506..83216fc1 100644 --- a/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs +++ b/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs @@ -8,33 +8,32 @@ public abstract class TransformationProviderSimpleBase : TransformationProviderB public void AddDefaultTable() { Provider.AddTable("TestTwo", - new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey), + new Column("Id",DbType.Int32){IsNullable = false}, new Column("TestId", DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + "TestTwo", "Id") ); } public void AddTable() { Provider.AddTable("Test", - new Column("Id", DbType.Int32, ColumnProperty.NotNull), - new Column("Title", DbType.String, 100, ColumnProperty.Null), - new Column("name", DbType.String, 50, ColumnProperty.Null), - new Column("blobVal", DbType.Binary, ColumnProperty.Null), - new Column("boolVal", DbType.Boolean, ColumnProperty.Null), - new Column("bigstring", DbType.String, 50000, ColumnProperty.Null) - ); + new Column("Id",DbType.Int32){IsNullable = false}, + new Column("Title",DbType.String,100), + new Column("name",DbType.String,50), + new Column("blobVal",DbType.Binary), + new Column("boolVal",DbType.Boolean), + new Column("bigstring",DbType.String,50000) ); } public void AddTableWithPrimaryKey() { Provider.AddTable("Test", - new Column("Id", DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), - new Column("Title", DbType.String, 100, ColumnProperty.Null), - new Column("name", DbType.String, 50, ColumnProperty.NotNull), + new Column("Id",DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column("Title",DbType.String,100), + new Column("name",DbType.String,50){IsNullable = false}, new Column("blobVal", DbType.Binary), new Column("boolVal", DbType.Boolean), new Column("bigstring", DbType.String, 50000) - ); +,new PrimaryKeyConstraint("PK_" + "Test", "Id") ); } public void AddPrimaryKey() diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddPrimaryKey.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddPrimaryKey.cs index b203a11c..256ecb7e 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddPrimaryKey.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddPrimaryKey.cs @@ -19,9 +19,9 @@ public void AddPrimaryKey_IdentityColumnWithData_Success() const string columnName2 = "TestColumn2"; Provider.AddTable(tableName, - new Column(columnName1, DbType.Int32, property: ColumnProperty.Identity | ColumnProperty.PrimaryKey), + new Column(columnName1,DbType.Int32){IsNullable = false,IsIdentity = true}, new Column(columnName2, DbType.String) - ); +,new PrimaryKeyConstraint("PK_" + tableName, columnName1) ); // Act Provider.Insert(tableName, [columnName2], ["Hello"]); @@ -54,9 +54,8 @@ public void AddPrimaryKey_AddPrimaryKey_ShouldStillBeNotNull() const string columnName2 = "TestColumn2"; Provider.AddTable(tableName, - new Column(columnName1, DbType.Int32, property: ColumnProperty.NotNull), - new Column(columnName2, DbType.DateTime, property: ColumnProperty.NotNull) - ); + new Column(columnName1,DbType.Int32){IsNullable = false}, + new Column(columnName2,DbType.DateTime){IsNullable = false} ); // Act Provider.AddPrimaryKey(name: "MyPkName", table: tableName, columnName1); @@ -65,7 +64,7 @@ public void AddPrimaryKey_AddPrimaryKey_ShouldStillBeNotNull() var column1 = Provider.GetColumnByName(table: tableName, column: columnName1); var column2 = Provider.GetColumnByName(table: tableName, column: columnName2); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); - Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsNullable, Is.False); + Assert.That(column2.IsNullable, Is.False); } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs index 0c78c68b..2c494152 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs @@ -20,16 +20,15 @@ public void AddTable_PrimaryKeyWithIdentity_Success() // Act Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.PrimaryKeyWithIdentity), - new Column(column2Name, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(column1Name,DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(column2Name,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, column1Name) ); // Assert var column1 = Provider.GetColumnByName(tableName, column1Name); var column2 = Provider.GetColumnByName(tableName, column2Name); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); - Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsIdentity, Is.True); + Assert.That(column2.IsNullable, Is.False); } [Test] @@ -42,16 +41,15 @@ public void AddTable_PrimaryKeyAndIdentity_Success() // Act Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.PrimaryKey | ColumnProperty.Identity), - new Column(column2Name, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(column1Name,DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(column2Name,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, column1Name) ); // Assert var column1 = Provider.GetColumnByName(tableName, column1Name); var column2 = Provider.GetColumnByName(tableName, column2Name); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); - Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsIdentity, Is.True); + Assert.That(column2.IsNullable, Is.False); } [Test] @@ -64,9 +62,8 @@ public void AddTable_PrimaryKeyAndIdentityWithInsertNull_Success() // Act Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.PrimaryKey | ColumnProperty.Identity), - new Column(column2Name, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(column1Name,DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(column2Name,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, column1Name) ); Provider.Insert(table: tableName, [column2Name], [999]); @@ -86,8 +83,8 @@ public void AddTable_PrimaryKeyAndIdentityWithInsertNull_Success() Assert.That(records.Single().Item1, Is.EqualTo(1)); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); - Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsIdentity, Is.True); + Assert.That(column2.IsNullable, Is.False); } [Test] @@ -100,16 +97,15 @@ public void AddTable_PrimaryKeyAndIdentityWithoutNotNull_Success() // Act Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.Identity), - new Column(column2Name, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(column1Name,DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(column2Name,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, column1Name) ); // Assert var column1 = Provider.GetColumnByName(tableName, column1Name); var column2 = Provider.GetColumnByName(tableName, column2Name); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); - Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsIdentity, Is.True); + Assert.That(column2.IsNullable, Is.False); } [Test] @@ -121,13 +117,12 @@ public void AddTable_NotNull_Success() // Act Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(column1Name,DbType.Int32){IsNullable = false} ); // Assert var column1 = Provider.GetColumnByName(tableName, column1Name); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsNullable, Is.False); } @@ -135,9 +130,8 @@ public void AddTable_NotNull_Success() public void AddTableWithCompoundPrimaryKey() { Provider.AddTable("Test", - new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey) - ); + new Column("PersonId",DbType.Int32){IsNullable = false}, + new Column("AddressId",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "Test", "PersonId", "AddressId") ); Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); diff --git a/src/Migrator.Tests/Providers/Generic/Generic_ChangeColumnTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_ChangeColumnTestsBase.cs index a1902a26..72f26ab3 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_ChangeColumnTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_ChangeColumnTestsBase.cs @@ -32,18 +32,17 @@ public void ChangeColumn_NotNullAndNullToNotNull_Success() // Act Provider.AddTable(tableName, - new Column(column1Name, DbType.DateTime, ColumnProperty.NotNull), - new Column(column2Name, DbType.DateTime, ColumnProperty.Null) - ); + new Column(column1Name,DbType.DateTime){IsNullable = false}, + new Column(column2Name,DbType.DateTime) ); // Assert - Provider.ChangeColumn(tableName, new Column(column1Name, DbType.DateTime2, ColumnProperty.NotNull)); - Provider.ChangeColumn(tableName, new Column(column2Name, DbType.DateTime2, ColumnProperty.NotNull)); + Provider.ChangeColumn(tableName, new Column(column1Name,DbType.DateTime2){IsNullable = false}); + Provider.ChangeColumn(tableName, new Column(column2Name,DbType.DateTime2){IsNullable = false}); var column1 = Provider.GetColumnByName(tableName, column1Name); var column2 = Provider.GetColumnByName(tableName, column2Name); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); - Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsNullable, Is.False); + Assert.That(column2.IsNullable, Is.False); } [Test] @@ -57,13 +56,12 @@ public void ChangeColumn_RemoveDefaultValue_Success() var testTime = new DateTime(2025, 5, 5, 5, 5, 5, DateTimeKind.Utc); Provider.AddTable(tableName, - new Column(name: column1Name, type: DbType.Int32, property: ColumnProperty.NotNull), - new Column(name: column2Name, type: DbType.DateTime2, property: ColumnProperty.Null, defaultValue: testTime) - ); + new Column(name: column1Name,type: DbType.Int32){IsNullable = false}, + new Column(name: column2Name,type: DbType.DateTime2,defaultValue: testTime) ); // Act Provider.Insert(table: tableName, [column1Name], [1]); - Provider.ChangeColumn(table: tableName, column: new Column(name: column2Name, type: DbType.DateTime2, property: ColumnProperty.Null)); + Provider.ChangeColumn(table: tableName, column: new Column(name: column2Name,type: DbType.DateTime2)); // Assert Provider.Insert(table: tableName, [column1Name], [2]); diff --git a/src/Migrator.Tests/Providers/Generic/Generic_ConstraintExistsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_ConstraintExistsBase.cs index a55e1aaa..75e0a05f 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_ConstraintExistsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_ConstraintExistsBase.cs @@ -19,13 +19,11 @@ public void ConstraintExists_ForeignKeyExists_ReturnsTrue() var fkName = "FK_Task_TaskGroup"; Provider.AddTable("Task", - new Column(name: "Id", type: DbType.Int32, property: ColumnProperty.PrimaryKey), - new Column(name: "TaskGroupId", type: DbType.Int32, property: ColumnProperty.Null) - ); + new Column(name: "Id",type: DbType.Int32){IsNullable = false}, + new Column(name: "TaskGroupId",type: DbType.Int32),new PrimaryKeyConstraint("PK_" + "Task", "Id") ); Provider.AddTable("TaskGroup", - new Column(name: "Id", type: DbType.Int32, property: ColumnProperty.PrimaryKey) - ); + new Column(name: "Id",type: DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "TaskGroup", "Id") ); Provider.AddForeignKey(name: fkName, childTable: tableName, childColumn: "TaskGroupId", parentTable: "TaskGroup", parentColumn: "Id"); diff --git a/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs index 58d525bb..5a154f4d 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs @@ -14,10 +14,9 @@ public void DefaultValue_Null_Success() const string columnName1Target = "TargetColumn1"; Provider.AddTable(tableNameSource, - new Column(columnName1Target, DbType.Int32, ColumnProperty.Null, null) - ); + new Column(columnName1Target,DbType.Int32,null) ); - Provider.ChangeColumn(tableNameSource, new Column(columnName1Target, DbType.Int32, ColumnProperty.NotNull)); + Provider.ChangeColumn(tableNameSource, new Column(columnName1Target,DbType.Int32){IsNullable = false}); } [Test] @@ -27,10 +26,9 @@ public void DefaultValue_ConvertStringToNotNull_DoesNotThrow() const string columnName1Target = "TargetColumn1"; Provider.AddTable(tableNameSource, - new Column(columnName1Target, DbType.String, 32, ColumnProperty.NotNull) - ); + new Column(columnName1Target,DbType.String,32){IsNullable = false} ); - Provider.ChangeColumn(tableNameSource, new Column(columnName1Target, DbType.String, ColumnProperty.Null)); + Provider.ChangeColumn(tableNameSource, new Column(columnName1Target,DbType.String)); } [Test] @@ -40,11 +38,10 @@ public void RemoveColumnDefaultValue_DoesNotThrow() const string columnName1 = "ColumnName1"; Provider.AddTable(tableNameSource, - new Column(columnName1, DbType.Int32, ColumnProperty.NotNull, 10) - ); + new Column(columnName1, DbType.Int32) { DefaultValue = 10, IsNullable = false} ); Provider.RemoveColumnDefaultValue(tableNameSource, columnName1); - Provider.ChangeColumn(tableNameSource, new Column(columnName1, DbType.Int32, ColumnProperty.Null)); + Provider.ChangeColumn(tableNameSource, new Column(columnName1,DbType.Int32)); } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs index 6be8fd58..3eb1d250 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_GetColumnsTestsBase.cs @@ -18,19 +18,11 @@ public void NamedTableConstraintsEnforceCompositeKeysAndReturnOrderedMetadata() 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 })); } @@ -40,7 +32,7 @@ public void CompositeUniqueDoesNotMarkItsIndividualColumnsUnique() { Provider.AddTable("CompositeUniqueMetadata", new Column("FirstId", DbType.Int32), new Column("SecondId", DbType.Int32)); Provider.AddUniqueConstraint("CompositeUniqueKey", "CompositeUniqueMetadata", "FirstId", "SecondId"); - Assert.That(Provider.GetColumns("CompositeUniqueMetadata").All(c => !c.ColumnProperty.HasFlag(ColumnProperty.Unique)), Is.True); + Assert.That(Provider.GetTableConstraints("CompositeUniqueMetadata").OfType().Single().KeyColumns.Length == 2, Is.True); } [Test] @@ -48,13 +40,13 @@ public void GetColumns_UniqueButNotPrimaryKey_ReturnsFalse() { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Id", DbType.Int32, ColumnProperty.Unique)); + Provider.AddTable(tableName, new Column("Id",DbType.Int32),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + tableName + "_" + "Id", "Id")); // Act var columns = Provider.GetColumns(tableName); // Assert - Assert.That(columns.Single().ColumnProperty, Is.EqualTo(ColumnProperty.Null | ColumnProperty.Unique)); + Assert.That(columns.Single().IsNullable, Is.True); } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/Generic/Generic_UpdateFromTableToTableTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_UpdateFromTableToTableTestsBase.cs index dce4047e..1bc28246 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_UpdateFromTableToTableTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_UpdateFromTableToTableTestsBase.cs @@ -29,8 +29,8 @@ public void UpdateFromTableToTable_Success() Provider.AddTable(tableNameSource, - new Column(columnName1Source, DbType.Int32, ColumnProperty.NotNull), - new Column(columnName2Source, DbType.Int32, ColumnProperty.NotNull), + new Column(columnName1Source,DbType.Int32){IsNullable = false}, + new Column(columnName2Source,DbType.Int32){IsNullable = false}, new Column(columnName3Source, DbType.String), new Column(columnName4Source, DbType.String), new Column(columnName5Source, DbType.String) @@ -39,8 +39,8 @@ public void UpdateFromTableToTable_Success() Provider.AddPrimaryKey("PK_Source", tableNameSource, [columnName1Source, columnName2Source]); Provider.AddTable(tableNameTarget, - new Column(columnName1Target, DbType.Int32, ColumnProperty.NotNull), - new Column(columnName2Target, DbType.Int32, ColumnProperty.NotNull), + new Column(columnName1Target,DbType.Int32){IsNullable = false}, + new Column(columnName2Target,DbType.Int32){IsNullable = false}, new Column(columnName3Target, DbType.String), new Column(columnName4Target, DbType.String), new Column(columnName5Target, DbType.String) diff --git a/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs index d2d2a180..f5ce8c8d 100644 --- a/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs +++ b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs @@ -56,7 +56,8 @@ public void CanAddPrimaryKey() [Test] public void AddUniqueColumn() { - Provider.AddColumn("TestTwo", "Test", DbType.String, 50, ColumnProperty.Unique); + Provider.AddColumn("TestTwo", new Column("Test", DbType.String, 50)); + Provider.AddUniqueConstraint("UQ_TestTwo_Test", "TestTwo", "Test"); } [Test] @@ -149,17 +150,16 @@ public void AddTableWithCompoundPrimaryKeyShouldKeepNullForOtherProperties() var testTableName = "Test"; Provider.AddTable(testTableName, - new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("Name", DbType.String, 30, ColumnProperty.Null) - ); + new Column("PersonId",DbType.Int32){IsNullable = false}, + new Column("AddressId",DbType.Int32){IsNullable = false}, + new Column("Name",DbType.String,30),new PrimaryKeyConstraint("PK_" + testTableName, "PersonId", "AddressId") ); Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); var column = Provider.GetColumnByName("Test", "Name"); Assert.That(column, Is.Not.Null); - Assert.That((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null, Is.True); + Assert.That(column.IsNullable, Is.True); } [Test] @@ -173,13 +173,12 @@ public void GetForeignKeyConstraints_SingleColumn_Success() const string parentIdColumn = "ParentId"; Provider.AddTable(parentTableName, - new Column(idColumn, DbType.Int32, ColumnProperty.PrimaryKey) - ); + new Column(idColumn,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + parentTableName, idColumn) ); Provider.AddTable(childTableName, - new Column(idColumn, DbType.Int32, ColumnProperty.PrimaryKey), + new Column(idColumn,DbType.Int32){IsNullable = false}, new Column(parentIdColumn, DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + childTableName, idColumn) ); Provider.AddForeignKey(fkName, childTableName, parentIdColumn, parentTableName, idColumn); @@ -210,14 +209,13 @@ public void GetForeignKeyConstraints_MultiColumnColumn_Success() const string childColumnParentTest = "ParentTest"; Provider.AddTable(parentTableName, - new Column(parentColumnId, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(parentColumnTest, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(parentColumnId,DbType.Int32){IsNullable = false}, + new Column(parentColumnTest,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + parentTableName, parentColumnId) ); Provider.AddTable(childTableName, - new Column(childColumnParentId, DbType.Int32, ColumnProperty.PrimaryKey), + new Column(childColumnParentId,DbType.Int32){IsNullable = false}, new Column(childColumnParentTest, DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + childTableName, childColumnParentId) ); Provider.AddUniqueConstraint("MyUniqueConstraint", parentTableName, [parentColumnId, parentColumnTest]); diff --git a/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscTests.cs b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscTests.cs index 62249ae1..3d65881d 100644 --- a/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscTests.cs +++ b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscTests.cs @@ -78,11 +78,11 @@ public void GetColumnsContainsProperNullInformation() { if (column.Name == "name") { - Assert.That((column.ColumnProperty & ColumnProperty.NotNull) == ColumnProperty.NotNull, Is.True); + Assert.That(!column.IsNullable, Is.True); } else if (column.Name == "Title") { - Assert.That((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null, Is.True); + Assert.That(column.IsNullable, Is.True); } } } @@ -170,9 +170,9 @@ public void ChangeColumn() [Test] public void ChangeColumn_FromNullToNull() { - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); + Provider.ChangeColumn("TestTwo", new Column("TestId",DbType.String,50)); + Provider.ChangeColumn("TestTwo", new Column("TestId",DbType.String,50)); + Provider.ChangeColumn("TestTwo", new Column("TestId",DbType.String,50)); Provider.Insert("TestTwo", ["Id", "TestId"], [2, "Not an Int val."]); } @@ -186,7 +186,7 @@ public void AddDecimalColumn() [Test] public void AddColumnWithDefault() { - Provider.AddColumn("TestTwo", "TestWithDefault", DbType.Int32, 50, 0, 10); + Provider.AddColumn("TestTwo", new Column("TestWithDefault", DbType.Int32, 50) { DefaultValue = 10 }); Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefault"), Is.True); } @@ -203,21 +203,21 @@ public void AddColumnWithDefaultButNoSize() [Test] public void AddBooleanColumnWithDefault() { - Provider.AddColumn("TestTwo", "TestBoolean", DbType.Boolean, 0, 0, false); + Provider.AddColumn("TestTwo", new Column("TestBoolean", DbType.Boolean) { DefaultValue = false }); Assert.That(Provider.ColumnExists("TestTwo", "TestBoolean"), Is.True); } [Test] public void CanGetNullableFromProvider() { - Provider.AddColumn("TestTwo", "NullableColumn", DbType.String, 30, ColumnProperty.Null); + Provider.AddColumn("TestTwo", new Column("NullableColumn", DbType.String, 30)); var columns = Provider.GetColumns("TestTwo"); foreach (var column in columns) { if (column.Name == "NullableColumn") { - Assert.That((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null, Is.True); + Assert.That(column.IsNullable, Is.True); } } } diff --git a/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs b/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs index a0aecf29..aba5077d 100644 --- a/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs +++ b/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs @@ -202,9 +202,9 @@ internal void AssertDatabaseError(TestDelegate action) } private void CreateItems() => provider.AddTable("items", - new Column("id", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("label", DbType.String, 40, ColumnProperty.Null), - new Column("amount", DbType.Int32, ColumnProperty.NotNull, 7)); + new Column("id",DbType.Int32){IsNullable = false}, + new Column("label",DbType.String,40), + new Column("amount", DbType.Int32) { DefaultValue = 7, IsNullable = false},new PrimaryKeyConstraint("PK_" + "items", "id")); [Test] public void TableAndColumnMetadata() @@ -217,8 +217,8 @@ public void TableAndColumnMetadata() var columns = provider.GetColumns("items"); Assert.That(columns, Has.Length.EqualTo(3)); Assert.That(columns.Single(c => c.Name.Equals("id", StringComparison.OrdinalIgnoreCase)).Type, Is.EqualTo(DbType.Int32)); - Assert.That(columns.Single(c => c.Name.Equals("label", StringComparison.OrdinalIgnoreCase)).ColumnProperty.HasFlag(ColumnProperty.Null), Is.True); - Assert.That(columns.Single(c => c.Name.Equals("amount", StringComparison.OrdinalIgnoreCase)).ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(columns.Single(c => c.Name.Equals("label", StringComparison.OrdinalIgnoreCase)).IsNullable, Is.True); + Assert.That(columns.Single(c => c.Name.Equals("amount", StringComparison.OrdinalIgnoreCase)).IsNullable, Is.False); provider.RemoveTable("items"); Assert.That(provider.TableExists("items"), Is.False); } @@ -257,12 +257,12 @@ public void DataDefaultsAndPersistence() public void AddRenameChangeAndDropColumn() { CreateItems(); - provider.AddColumn("items", new Column("extra", DbType.String, 20, ColumnProperty.Null)); + provider.AddColumn("items", new Column("extra",DbType.String,20)); provider.RenameColumn("items", "extra", "renamed"); - provider.ChangeColumn("items", new Column("renamed", DbType.String, 80, ColumnProperty.NotNull, "fallback")); + provider.ChangeColumn("items", new Column("renamed",DbType.String,80,"fallback"){IsNullable = false}); var changed = provider.GetColumns("items").Single(c => c.Name.Equals("renamed", StringComparison.OrdinalIgnoreCase)); Assert.That(changed.Size, Is.EqualTo(80)); - Assert.That(changed.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(changed.IsNullable, Is.False); provider.Insert("items", ["id"], [1]); Assert.That(provider.ExecuteScalar("SELECT renamed FROM items"), Is.EqualTo("fallback")); provider.RemoveColumnDefaultValue("items", "renamed"); @@ -275,18 +275,18 @@ public void AddRenameChangeAndDropColumn() public void PrimaryKeyAndIdentity() { provider.AddTable("items", - new Column("id", DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), - new Column("label", DbType.String, 40)); + new Column("id",DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column("label", DbType.String, 40),new PrimaryKeyConstraint("PK_" + "items", "id")); provider.Insert("items", ["label"], ["first"]); provider.Insert("items", ["label"], ["second"]); Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT COUNT(DISTINCT id) FROM items")), Is.EqualTo(2)); - Assert.That(provider.GetColumns("items").Single(c => c.Name.Equals("id", StringComparison.OrdinalIgnoreCase)).ColumnProperty.HasFlag(ColumnProperty.Identity), Is.True); + Assert.That(provider.GetColumns("items").Single(c => c.Name.Equals("id", StringComparison.OrdinalIgnoreCase)).IsIdentity, Is.True); } [Test] public void NamedPrimaryKey() { - provider.AddTable("items", new Column("id", DbType.Int32, ColumnProperty.NotNull)); + provider.AddTable("items", new Column("id",DbType.Int32){IsNullable = false}); provider.AddPrimaryKey("pk_items", "items", "id"); Assert.That(provider.PrimaryKeyExists("items", "pk_items"), Is.True); provider.Insert("items", ["id"], [1]); @@ -316,7 +316,7 @@ public void UniqueAndCheckConstraints() { CreateItems(); // Db2 requires NOT NULL for columns participating in a UNIQUE constraint. - provider.ChangeColumn("items", new Column("label", DbType.String, 40, ColumnProperty.NotNull)); + provider.ChangeColumn("items", new Column("label",DbType.String,40){IsNullable = false}); provider.AddUniqueConstraint("uq_label", "items", "label"); provider.AddCheckConstraint("ck_amount", "items", "amount >= 0"); Assert.That(provider.ConstraintExists("items", "uq_label"), Is.True); diff --git a/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs b/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs index 18158f3b..ebbdddf4 100644 --- a/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs +++ b/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs @@ -56,9 +56,9 @@ public class LiveMetadataRegressionTests [TestCase("Sybase", ProviderTypes.Sybase, Category = "Sybase")] public void ChangeColumnCreatesRequestedUniqueConstraint(string database, ProviderTypes type) => new LiveDatabaseTests(database, type).RunRegression(f => { - f.Provider.AddTable("unique_values", new Column("amount", DbType.Int32, ColumnProperty.NotNull)); + f.Provider.AddTable("unique_values", new Column("amount",DbType.Int32){IsNullable = false}); f.Provider.Insert("unique_values", ["amount"], [7]); - f.Provider.ChangeColumn("unique_values", new Column("amount", DbType.Int64, ColumnProperty.NotNull | ColumnProperty.Unique)); + f.Provider.ChangeColumn("unique_values", new Column("amount",DbType.Int64){IsNullable = false}); Assert.That(f.Provider.ConstraintExists("unique_values", "UX_unique_values_amount"), Is.True); Assert.That(f.Provider.GetIndexes("unique_values").Any(i => i.UniqueConstraint && i.KeyColumns.Single().Equals("amount", StringComparison.OrdinalIgnoreCase)), Is.True); f.AssertDatabaseError(() => f.Provider.Insert("unique_values", ["amount"], [7L])); @@ -122,12 +122,12 @@ public class LiveMetadataRegressionTests [TestCase("Sybase", ProviderTypes.Sybase, Category = "Sybase")] public void DecimalShapeSurvivesCreateAlterAndCopy(string database, ProviderTypes type) => new LiveDatabaseTests(database, type).RunRegression(f => { - f.Provider.AddTable("numbers", new Column("amount", DbType.Decimal, ColumnProperty.Null) { Precision = 12, Scale = 3 }); + f.Provider.AddTable("numbers", new Column("amount",DbType.Decimal){Precision = 12,Scale = 3 }); var original = f.Provider.GetColumns("numbers").Single(); Assert.That(original.Precision, Is.EqualTo(12)); Assert.That(original.Scale, Is.EqualTo(3)); f.Provider.Insert("numbers", ["amount"], [123.456m]); - f.Provider.ChangeColumn("numbers", new Column("amount", DbType.Decimal, ColumnProperty.Null) { Precision = 15, Scale = 3 }); + f.Provider.ChangeColumn("numbers", new Column("amount",DbType.Decimal){Precision = 15,Scale = 3 }); var changed = f.Provider.GetColumns("numbers").Single(); Assert.That(changed.Precision, Is.EqualTo(15)); Assert.That(changed.Scale, Is.EqualTo(3)); @@ -136,7 +136,7 @@ public class LiveMetadataRegressionTests var copied = f.Provider.GetColumns("copied_numbers").Single(); Assert.That(copied.Precision, Is.EqualTo(15)); Assert.That(copied.Scale, Is.EqualTo(3)); - f.Provider.AddColumn("copied_numbers", new Column("extra", DbType.Decimal, ColumnProperty.Null) { Precision = 10, Scale = 2 }); + f.Provider.AddColumn("copied_numbers", new Column("extra",DbType.Decimal){Precision = 10,Scale = 2 }); var added = f.Provider.GetColumns("copied_numbers").Single(c => c.Name.Equals("extra", StringComparison.OrdinalIgnoreCase)); Assert.That(added.Precision, Is.EqualTo(10)); Assert.That(added.Scale, Is.EqualTo(2)); @@ -147,12 +147,12 @@ public class LiveMetadataRegressionTests [TestCase("Sybase", ProviderTypes.Sybase, Category = "Sybase")] public void PrimaryKeyMetadataIncludesIdentityAndCompositeMembers(string database, ProviderTypes type) => new LiveDatabaseTests(database, type).RunRegression(f => { - f.Provider.AddTable("identities", new Column("id", DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity)); - Assert.That(f.Provider.GetColumns("identities").Single().ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); - f.Provider.AddTable("pairs", new Column("first_id", DbType.Int32, ColumnProperty.PrimaryKey), new Column("second_id", DbType.Int32, ColumnProperty.PrimaryKey), new Column("label", DbType.String, 20)); + f.Provider.AddTable("identities", new Column("id",DbType.Int32){IsNullable = false,IsIdentity = true},new PrimaryKeyConstraint("PK_" + "identities", "id")); + Assert.That(f.Provider.GetColumns("identities").Single().IsIdentity, Is.True); + f.Provider.AddTable("pairs", new Column("first_id",DbType.Int32){IsNullable = false}, new Column("second_id",DbType.Int32){IsNullable = false}, new Column("label", DbType.String, 20),new PrimaryKeyConstraint("PK_" + "pairs", "first_id", "second_id")); var columns = f.Provider.GetColumns("pairs"); - Assert.That(columns.Count(c => c.IsPrimaryKey), Is.EqualTo(2)); - Assert.That(columns.Single(c => c.Name.Equals("label", StringComparison.OrdinalIgnoreCase)).IsPrimaryKey, Is.False); + Assert.That(f.Provider.GetTableConstraints("pairs").OfType().Single().KeyColumns.Length, Is.EqualTo(2)); + Assert.That(f.Provider.GetTableConstraints("pairs").OfType().Single().KeyColumns.Any(c => c.Equals("label", StringComparison.OrdinalIgnoreCase)), Is.False); }); [Test, Category("Db2")] @@ -241,7 +241,7 @@ public class LiveMetadataRegressionTests [TestCase("Sybase", ProviderTypes.Sybase, Category = "Sybase")] public void InlineIndexedColumnCreatesIndex(string database, ProviderTypes type) => new LiveDatabaseTests(database, type).RunRegression(f => { - f.Provider.AddTable("indexed_values", new Column("amount", DbType.Int32, ColumnProperty.Indexed)); + f.Provider.AddTable("indexed_values", new Column("amount",DbType.Int32),new DotNetProjects.Migrator.Framework.Index { Name = "IX_" + "indexed_values" + "_" + "amount", KeyColumns = new[] { "amount" } }); Assert.That(f.Provider.GetIndexes("indexed_values").Any(i => i.KeyColumns.Select(c => c.ToLowerInvariant()).SequenceEqual(new[] { "amount" })), Is.True); }); @@ -299,7 +299,7 @@ public class LiveMetadataRegressionTests [Test, Category("Informix")] public void InformixRemovesConstraintBackedIndexes() => new LiveDatabaseTests("Informix", ProviderTypes.IBM_Informix).RunRegression(f => { - f.Provider.AddTable("numbers", new Column("id", DbType.Int32, ColumnProperty.NotNull), new Column("amount", DbType.Int32, ColumnProperty.NotNull)); + f.Provider.AddTable("numbers", new Column("id",DbType.Int32){IsNullable = false}, new Column("amount",DbType.Int32){IsNullable = false}); f.Provider.AddPrimaryKey("pk_numbers", "numbers", "id"); f.Provider.AddUniqueConstraint("uq_amount", "numbers", "amount"); Assert.That(f.Provider.GetIndexes("numbers").Count(i => i.PrimaryKey), Is.EqualTo(1)); diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs index 5f56b686..aac7a5eb 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs @@ -22,9 +22,9 @@ public async Task SetUpAsync() public void ChangeColumn_FromNotNullToNotNull() { Provider.ExecuteNonQuery("DELETE FROM TestTwo"); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); + Provider.ChangeColumn("TestTwo", new Column("TestId",DbType.String,50)); Provider.Insert("TestTwo", ["Id", "TestId"], [3, "Not an Int val."]); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.NotNull)); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.NotNull)); + Provider.ChangeColumn("TestTwo", new Column("TestId",DbType.String,50){IsNullable = false}); + Provider.ChangeColumn("TestTwo", new Column("TestId",DbType.String,50){IsNullable = false}); } } diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddColumnTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddColumnTests.cs index 6675de98..6b652d92 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddColumnTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddColumnTests.cs @@ -26,18 +26,17 @@ public void AddTable_NotNull_OtherColumnStillNotNull() Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(column1Name,DbType.Int32){IsNullable = false} ); // Act - Provider.AddColumn(table: tableName, column: new Column(column2Name, DbType.DateTime, ColumnProperty.NotNull)); + Provider.AddColumn(table: tableName, column: new Column(column2Name,DbType.DateTime){IsNullable = false}); // Assert var column1 = Provider.GetColumnByName(tableName, column1Name); var column2 = Provider.GetColumnByName(tableName, column2Name); - Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); - Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + Assert.That(column1.IsNullable, Is.False); + Assert.That(column2.IsNullable, Is.False); } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_GetColumns_Tests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_GetColumns_Tests.cs index fbc6fed7..2bd2e1e9 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_GetColumns_Tests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_GetColumns_Tests.cs @@ -124,8 +124,8 @@ public void GetColumns_GetIdentity_Succeeds() Provider.ExecuteNonQuery($"CREATE TABLE {tableName1} ({columnName1} NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY)"); Provider.ExecuteNonQuery($"CREATE TABLE {tableName2} ({columnName1} NUMBER PRIMARY KEY)"); - Provider.AddTable(name: tableName3, new Column(columnName1, DbType.Int32, ColumnProperty.Identity | ColumnProperty.PrimaryKey)); - Provider.AddTable(name: tableName4, new Column(columnName1, DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(name: tableName3, new Column(columnName1,DbType.Int32){IsNullable = false,IsIdentity = true},new PrimaryKeyConstraint("PK_" + tableName3, columnName1)); + Provider.AddTable(name: tableName4, new Column(columnName1,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName4, columnName1)); // Act var columnTable1 = Provider.GetColumnByName(table: tableName1, column: columnName1); diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_TableExistsTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_TableExistsTests.cs index 40712dbb..2d6a4187 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_TableExistsTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_TableExistsTests.cs @@ -14,7 +14,7 @@ public class OracleTransformationProvider_TableExistsTests : OracleTransformatio [Test] public void LegacyForeignKeyOverloadHonorsCascadeDelete() { - Provider.AddTable("CascadeParent", new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable("CascadeParent", new Column("Id",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "CascadeParent", "Id")); Provider.AddTable("CascadeChild", new Column("ParentId", DbType.Int32)); Provider.AddForeignKey("CascadeForeignKey", "CascadeChild", new[] { "ParentId" }, "CascadeParent", new[] { "Id" }, ForeignKeyConstraintType.Cascade); Provider.Insert("CascadeParent", new[] { "Id" }, new object[] { 1 }); diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index d271bf58..db1d7070 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -28,9 +28,8 @@ public async Task SetUpAsync() public void AddTableWithCompoundPrimaryKey() { Provider.AddTable("Test", - new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey) - ); + new Column("PersonId",DbType.Int32){IsNullable = false}, + new Column("AddressId",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "Test", "PersonId", "AddressId") ); Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); @@ -321,14 +320,12 @@ public void AddIndex_TableNameIsReservedWord_Succeeds() { // Arrange Provider.AddTable("trigger", - new Column(name: "id", type: DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), - new Column(name: "test_run_id", type: DbType.Int32, ColumnProperty.NotNull) - ); + new Column(name: "id",type: DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(name: "test_run_id",type: DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "trigger", "id") ); Provider.AddTable("statistics", - new Column(name: "id", type: DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), - new Column(name: "test_run_id", type: DbType.Int32, ColumnProperty.NotNull) - ); + new Column(name: "id",type: DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(name: "test_run_id",type: DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "statistics", "id") ); // Act var addIndexTriggerSql = Provider.AddIndex(name: "IX_trigger__test_run_id", table: "trigger", "test_run_id"); diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs index 2d60120d..b8f92b1a 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs @@ -29,14 +29,13 @@ public void ChangeColumn_DateTimeOffsetToDateTime_Success() // Act Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.Null), - new Column(column2Name, DbType.DateTimeOffset, ColumnProperty.Null, defaultValue: dateTimeDefaultValue) - ); + new Column(column1Name,DbType.Int32), + new Column(column2Name,DbType.DateTimeOffset,defaultValue: dateTimeDefaultValue) ); Provider.Insert(table: tableName, columns: [column2Name], values: [dateTimeInsert]); // Assert - Provider.ChangeColumn(tableName, new Column(column2Name, DbType.DateTime2, ColumnProperty.NotNull)); + Provider.ChangeColumn(tableName, new Column(column2Name,DbType.DateTime2){IsNullable = false}); var column2 = Provider.GetColumnByName(tableName, column2Name); Assert.That(column2.MigratorDbType, Is.EqualTo(MigratorDbType.DateTime2)); @@ -54,16 +53,15 @@ public void ChangeColumn_DateTimeOffsetToDateTimeGetDefaultValueAndReuseIt_Defau var dateTimeOffsetInsert = new DateTimeOffset(2001, 2, 3, 4, 5, 6, TimeSpan.FromHours(2)); Provider.AddTable(tableName, - new Column(column1Name, DbType.Int32, ColumnProperty.Null), - new Column(column2Name, DbType.DateTimeOffset, ColumnProperty.Null, defaultValue: dateTimeOffsetDefaultValue) - ); + new Column(column1Name,DbType.Int32), + new Column(column2Name,DbType.DateTimeOffset,defaultValue: dateTimeOffsetDefaultValue) ); Provider.Insert(table: tableName, columns: [column2Name], values: [dateTimeOffsetInsert]); // Act var column2 = Provider.GetColumnByName(tableName, column2Name); Assert.That(((DateTimeOffset)column2.DefaultValue).UtcDateTime, Is.EqualTo(dateTimeOffsetDefaultValue.UtcDateTime)); - Provider.ChangeColumn(tableName, new Column(column2Name, DbType.DateTime2, ColumnProperty.NotNull, defaultValue: column2.DefaultValue)); + Provider.ChangeColumn(tableName, new Column(column2Name,DbType.DateTime2,defaultValue: column2.DefaultValue){IsNullable = false}); // Assert @@ -89,8 +87,8 @@ public void GetColumns_GetIdentity_Succeeds() Provider.ExecuteNonQuery($"CREATE TABLE {tableName1} ({columnName1} INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY)"); Provider.ExecuteNonQuery($"CREATE TABLE {tableName2} ({columnName1} INT PRIMARY KEY)"); - Provider.AddTable(name: tableName3, new Column(columnName1, DbType.Int32, ColumnProperty.Identity | ColumnProperty.PrimaryKey)); - Provider.AddTable(name: tableName4, new Column(columnName1, DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(name: tableName3, new Column(columnName1,DbType.Int32){IsNullable = false,IsIdentity = true},new PrimaryKeyConstraint("PK_" + tableName3, columnName1)); + Provider.AddTable(name: tableName4, new Column(columnName1,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName4, columnName1)); // Act var columnTable1 = Provider.GetColumnByName(table: tableName1, column: columnName1); diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_MetadataScopeTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_MetadataScopeTests.cs index 4a0e1b03..4ed76b0e 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_MetadataScopeTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_MetadataScopeTests.cs @@ -23,7 +23,7 @@ public void QualifiedMetadataDoesNotMixSameNamedTablesOrConstraints() Assert.That(Provider.ColumnExists("metadata_b.sample", "id"), Is.False); Assert.That(Provider.ConstraintExists("metadata_a.sample", "same_name"), Is.True); Assert.That(Provider.ConstraintExists("metadata_b.sample", "same_name"), Is.False); - Assert.That(Provider.GetColumns("metadata_a.sample").Single().ColumnProperty.IsSet(ColumnProperty.Unique), Is.True); + Assert.That(Provider.GetTableConstraints("metadata_a.sample").OfType().Any(), Is.True); Assert.That(Provider.GetColumns("metadata_b.sample").Single().MigratorDbType, Is.EqualTo(MigratorDbType.String)); Provider.ExecuteNonQuery("SET LOCAL search_path TO metadata_b"); Assert.That(Provider.GetColumns("sample").Single().Name, Is.EqualTo("value")); @@ -35,7 +35,7 @@ public void QuotedCatalogNamesRemainExactAndAreParameterized() Provider.ExecuteNonQuery("CREATE TABLE \"Meta'Table\" (id integer CONSTRAINT \"Key'Name\" UNIQUE)"); Assert.That(Provider.TableExists("\"Meta'Table\""), Is.True); Assert.That(Provider.ConstraintExists("\"Meta'Table\"", "Key'Name"), Is.True); - Assert.That(Provider.GetColumns("\"Meta'Table\"").Single().ColumnProperty.IsSet(ColumnProperty.Unique), Is.True); + Assert.That(Provider.GetTableConstraints("\"Meta'Table\"").OfType().Any(), Is.True); } [Test] diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs index 3eaaddf3..7a38a879 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs @@ -19,9 +19,8 @@ public void AddTableWithPrimaryKeyIdentity_Succeeds() const string propertyName2 = "Color2"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unsigned) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(propertyName2,DbType.Int32){IsUnsigned = true},new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); // Act Provider.Insert(testTableName, [propertyName2], [1]); diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs index 58b068c7..8dd92329 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs @@ -18,9 +18,8 @@ public void AddIndex_IncludeColumnsWithReservedWord_Succeeds() const string propertyName2 = "Host"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKeyWithIdentity), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unsigned) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(propertyName2,DbType.Int32){IsUnsigned = true},new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); // Act/Assert Provider.AddIndex(testTableName, new Index diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddTableTests.cs index 8272fcfa..5ff9d5fd 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddTableTests.cs @@ -20,9 +20,8 @@ public async Task SetUpAsync() public void AddTableWithCompoundPrimaryKey() { Provider.AddTable("Test", - new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey) - ); + new Column("PersonId",DbType.Int32){IsNullable = false}, + new Column("AddressId",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "Test", "PersonId", "AddressId") ); Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); @@ -34,7 +33,7 @@ public void AddTableDateTime() var tableName = "Table1"; var columnName = "Column1"; - Provider.AddTable(tableName, new Column(columnName, DbType.DateTime, ColumnProperty.NotNull)); + Provider.AddTable(tableName, new Column(columnName,DbType.DateTime){IsNullable = false}); var column = Provider.GetColumnByName(tableName, columnName); Assert.That(column.Type, Is.EqualTo(DbType.DateTime)); @@ -46,7 +45,7 @@ public void AddTableDateTime2() var tableName = "Table1"; var columnName = "Column1"; - Provider.AddTable(tableName, new Column(columnName, DbType.DateTime2, ColumnProperty.NotNull)); + Provider.AddTable(tableName, new Column(columnName,DbType.DateTime2){IsNullable = false}); var column = Provider.GetColumnByName(tableName, columnName); Assert.That(column.Type, Is.EqualTo(DbType.DateTime2)); diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs index 8712b4e3..fb511ef4 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs @@ -18,41 +18,34 @@ public async Task SetUpAsync() } [TestCase(false), TestCase(true)] - public void ChangeColumnRemovesOwnedUniqueFromTableOrColumnCreation(bool addColumn) + public void ChangeColumnPreservesExplicitUniqueFromTableOrColumnCreation(bool addColumn) { - var definition = new Column("Value", DbType.Int32, ColumnProperty.NotNull | ColumnProperty.Unique); + var definition = new Column("Value", DbType.Int32) { IsNullable = false }; if (addColumn) { Provider.AddTable("CreatedUnique", new Column("Id", DbType.Int32)); Provider.AddColumn("CreatedUnique", definition); + Provider.AddUniqueConstraint("UQ_Created", "CreatedUnique", "Value"); } - else Provider.AddTable("CreatedUnique", definition); - Provider.ChangeColumn("CreatedUnique", new Column("Value", DbType.Int32, ColumnProperty.NotNull)); + else Provider.AddTable("CreatedUnique", definition, + new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_Created", "Value")); + Provider.ChangeColumn("CreatedUnique", new Column("Value", DbType.Int32) { IsNullable = false }); + Assert.That(Provider.ConstraintExists("CreatedUnique", "UQ_Created"), Is.True); + Assert.That(definition.IsNullable, Is.False); Provider.Insert("CreatedUnique", new[] { "Value" }, new object[] { 1 }); - Provider.Insert("CreatedUnique", new[] { "Value" }, new object[] { 1 }); - Assert.That(definition.ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); - Assert.That(Provider.GetIndexes("CreatedUnique"), Is.Empty); - } - - [Test] - public void OwnershipAdoptionRejectsCompositeConstraints() - { - Provider.AddTable("CompositeOwned", new Column("FirstId", DbType.Int32), new Column("SecondId", DbType.Int32)); - Provider.AddUniqueConstraint("UserComposite", "CompositeOwned", "FirstId", "SecondId"); - Assert.Throws(() => ((SqlServerTransformationProvider)Provider).AdoptColumnUniqueConstraint("CompositeOwned", "FirstId", "UserComposite")); - Assert.That(Provider.ConstraintExists("CompositeOwned", "UserComposite"), Is.True); + Assert.Catch(() => Provider.Insert("CreatedUnique", new[] { "Value" }, new object[] { 1 })); } [Test] - public void ExplicitOwnershipAdoptionAllowsLegacyUniqueRemoval() + public void ExplicitUniqueRemovalAllowsDuplicates() { Provider.AddTable("LegacyUnique", new Column("Value", DbType.Int32)); Provider.AddUniqueConstraint("LegacyUniqueConstraint", "LegacyUnique", "Value"); - var sqlServer = (SqlServerTransformationProvider)Provider; - sqlServer.AdoptColumnUniqueConstraint("LegacyUnique", "Value", "LegacyUniqueConstraint"); - sqlServer.AdoptColumnUniqueConstraint("LegacyUnique", "Value", "LegacyUniqueConstraint"); - Provider.ChangeColumn("LegacyUnique", new Column("Value", DbType.Int32, ColumnProperty.Null)); - Assert.That(Provider.ConstraintExists("LegacyUnique", "LegacyUniqueConstraint"), Is.False); + Provider.ChangeColumn("LegacyUnique", new Column("Value", DbType.Int32)); + Assert.That(Provider.ConstraintExists("LegacyUnique", "LegacyUniqueConstraint"), Is.True); + Provider.RemoveConstraint("LegacyUnique", "LegacyUniqueConstraint"); + Provider.ExecuteNonQuery("INSERT INTO LegacyUnique VALUES (1), (1)"); + Assert.That(System.Convert.ToInt32(Provider.ExecuteScalar("SELECT COUNT(*) FROM LegacyUnique")), Is.EqualTo(2)); } [Test] @@ -62,11 +55,11 @@ public void ChangeColumn_DateTimeToDateTime2_Success() const string tableName = "TestTable"; const string columnName = "TestColumn"; - Provider.AddTable(tableName, new Column(columnName, DbType.DateTime, ColumnProperty.NotNull)); + Provider.AddTable(tableName, new Column(columnName,DbType.DateTime){IsNullable = false}); var columnBefore = Provider.GetColumnByName(tableName, columnName); // Act - Provider.ChangeColumn(tableName, new Column(columnName, DbType.DateTime2, ColumnProperty.NotNull)); + Provider.ChangeColumn(tableName, new Column(columnName,DbType.DateTime2){IsNullable = false}); // Assert var columnAfter = Provider.GetColumnByName(tableName, columnName); @@ -78,13 +71,13 @@ public void ChangeColumn_DateTimeToDateTime2_Success() [Test] public void ChangeColumn_DoesNotRemoveUserOwnedUniqueOrMutateDefinition() { - Provider.AddTable("UserOwned", new Column("Value", DbType.Int32, ColumnProperty.NotNull)); + Provider.AddTable("UserOwned", new Column("Value",DbType.Int32){IsNullable = false}); Provider.AddUniqueConstraint("UX_UserOwned_Value", "UserOwned", "Value"); - var definition = new Column("Value", DbType.Int32, ColumnProperty.NotNull, 3); + var definition = new Column("Value",DbType.Int32){IsNullable = false, DefaultValue = 3}; Provider.ChangeColumn("UserOwned", definition); Assert.That(Provider.ConstraintExists("UserOwned", "UX_UserOwned_Value"), Is.True); Assert.That(definition.DefaultValue, Is.EqualTo(3)); - Assert.That(definition.ColumnProperty, Is.EqualTo(ColumnProperty.NotNull)); + Assert.That(definition.IsNullable, Is.False); } [Test] @@ -94,11 +87,11 @@ public void ChangeColumn_WithUniqueThenReChangeToNonUnique_UniqueConstraintShoul const string tableName = "TestTable"; const string columnName = "TestColumn"; - Provider.AddTable(tableName, new Column(columnName, DbType.Int32, ColumnProperty.NotNull)); + Provider.AddTable(tableName, new Column(columnName,DbType.Int32){IsNullable = false}); // Act - Provider.ChangeColumn(tableName, new Column(columnName, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.Unique)); - Provider.ChangeColumn(tableName, new Column(columnName, DbType.Int32, ColumnProperty.NotNull)); + Provider.ChangeColumn(tableName, new Column(columnName,DbType.Int32){IsNullable = false}); + Provider.ChangeColumn(tableName, new Column(columnName,DbType.Int32){IsNullable = false}); // Assert var indexes = Provider.GetIndexes(tableName); diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_GetColumnsTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_GetColumnsTests.cs index 5d8ccc0e..50987586 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_GetColumnsTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_GetColumnsTests.cs @@ -29,8 +29,8 @@ public void GetColumns_GetIdentity_Succeeds() Provider.ExecuteNonQuery($"CREATE TABLE {tableName1} ({columnName1} INT IDENTITY(1,1) PRIMARY KEY)"); Provider.ExecuteNonQuery($"CREATE TABLE {tableName2} ({columnName1} INT PRIMARY KEY)"); - Provider.AddTable(name: tableName3, new Column(columnName1, DbType.Int32, ColumnProperty.Identity | ColumnProperty.PrimaryKey)); - Provider.AddTable(name: tableName4, new Column(columnName1, DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(name: tableName3, new Column(columnName1,DbType.Int32){IsNullable = false,IsIdentity = true},new PrimaryKeyConstraint("PK_" + tableName3, columnName1)); + Provider.AddTable(name: tableName4, new Column(columnName1,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName4, columnName1)); // Act var columnTable1 = Provider.GetColumnByName(table: tableName1, column: columnName1); diff --git a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderTests.cs b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderTests.cs index 074730a8..27d4843e 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderTests.cs @@ -17,7 +17,7 @@ public class SqlServerTransformationProviderTests : SQLServerTransformationProvi public void TimeTypeDefaultAndValueRoundTripThroughMetadata() { var time = new TimeSpan(0, 12, 34, 56, 789); - Provider.AddTable("ClockValues", new Column("Moment", DbType.Time, ColumnProperty.Null, time)); + Provider.AddTable("ClockValues", new Column("Moment",DbType.Time,time)); var column = Provider.GetColumns("ClockValues").Single(); Assert.That(column.Type, Is.EqualTo(DbType.Time)); Assert.That(column.DefaultValue, Is.EqualTo(time)); @@ -39,8 +39,8 @@ public void ExplicitScriptSplitsGoWithoutSplittingMultilineValues() [Test] public void IndependentForeignKeyActionsCascadeUpdateAndSetNullOnDelete() { - Provider.AddTable("ActionParent", new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.NotNull)); - Provider.AddTable("ActionChild", new Column("ParentId", DbType.Int32, ColumnProperty.Null)); + Provider.AddTable("ActionParent", new Column("Id",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "ActionParent", "Id")); + Provider.AddTable("ActionChild", new Column("ParentId",DbType.Int32)); ((IForeignKeyActions)Provider).AddForeignKey("ActionForeignKey", "ActionChild", new[] { "ParentId" }, "ActionParent", new[] { "Id" }, ForeignKeyConstraintType.SetNull, ForeignKeyConstraintType.Cascade); Provider.ExecuteNonQuery("INSERT INTO ActionParent VALUES (1); INSERT INTO ActionChild VALUES (1); UPDATE ActionParent SET Id=2 WHERE Id=1"); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs index 3d26a7c4..440bba13 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs @@ -76,16 +76,16 @@ public void AddPrimaryKey_CompositePrimaryKey_Succeeds() Provider.AddPrimaryKey("MyPrimaryKeyName", testTableName, "Id", "Color"); // Assert - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == "Id").ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == "Color").ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == "NotAPrimaryKey").ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains("Id") == true), Is.False); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains("Color") == true), Is.False); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains("NotAPrimaryKey") == true), Is.False); var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); var tableNames = ((SQLiteTransformationProvider)Provider).GetTables(); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == "Id").ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == "Color").ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == "NotAPrimaryKey").ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains("Id") == true), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains("Color") == true), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains("NotAPrimaryKey") == true), Is.False); // Check for intermediate table residues. Assert.That(tableNames.Where(x => x.Contains(testTableName)), Has.Exactly(1).Items); @@ -101,9 +101,9 @@ public void AddPrimaryKey_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() var indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.Unique | ColumnProperty.NotNull), + new Column(propertyName1,DbType.Int32){IsNullable = false}, new Column(propertyName2, DbType.Int32) - ); +,new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName1, propertyName1) ); Provider.AddIndex(indexName, testTableName, [propertyName1, propertyName2]); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -122,11 +122,11 @@ public void AddPrimaryKey_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.False); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName2) == true), Is.False); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName2) == true), Is.False); var indexAfter = tableInfoAfter.Indexes.Single(); Assert.That(indexAfter.Name, Is.EqualTo(indexName)); @@ -143,9 +143,8 @@ public void RemovePrimaryKey_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds( var indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName2, propertyName2) ); Provider.AddIndex(indexName, testTableName, [propertyName1, propertyName2]); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -164,11 +163,11 @@ public void RemovePrimaryKey_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds( var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.False); + Assert.That(tableInfoAfter.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); var indexAfter = tableInfoAfter.Indexes.Single(); Assert.That(indexAfter.Name, Is.EqualTo(indexName)); @@ -185,9 +184,9 @@ public void RemoveAllIndexes_HavingIndexAndUnique_RebuildSucceeds() var indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), + new Column(propertyName1,DbType.Int32){IsNullable = false}, new Column(propertyName2, DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); Provider.AddIndex(indexName, testTableName, [propertyName1, propertyName2]); Provider.AddUniqueConstraint("MyConstraint", testTableName, [propertyName1, propertyName2]); @@ -209,12 +208,12 @@ public void RemoveAllIndexes_HavingIndexAndUnique_RebuildSucceeds() var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); Assert.That(tableInfoBefore.Uniques, Is.Not.Empty); Assert.That(tableInfoBefore.Indexes, Is.Not.Empty); - Assert.That(tableInfoAfter.Uniques, Is.Empty); + Assert.That(tableInfoAfter.Uniques.Select(u => u.Name), Is.EquivalentTo(tableInfoBefore.Uniques.Select(u => u.Name).Append("MyUniqueConstraintName"))); Assert.That(tableInfoAfter.Indexes, Is.Empty); } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs index 4b9c6453..0b617734 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs @@ -26,9 +26,8 @@ public void AddColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() const string indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName2, propertyName2) ); Provider.AddIndex(indexName, testTableName, [propertyName1, propertyName2]); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -36,7 +35,7 @@ public void AddColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() Provider.ExecuteNonQuery($"INSERT INTO {testTableName} ({propertyName1}, {propertyName2}) VALUES (1, 2)"); // Act - Provider.AddColumn(table: testTableName, new Column(newColumn, DbType.String, ColumnProperty.Null)); + Provider.AddColumn(table: testTableName, new Column(newColumn,DbType.String)); Provider.ExecuteNonQuery($"INSERT INTO {testTableName} ({propertyName1}, {propertyName2}, {newColumn}) VALUES (2, 3, 'Hello')"); // Assert @@ -48,11 +47,11 @@ public void AddColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoAfter.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); var indexAfter = tableInfoAfter.Indexes.Single(); Assert.That(indexAfter.Name, Is.EqualTo(indexName)); @@ -73,10 +72,11 @@ public void AddColumn_HavingNullInPrimaryKey_HasNotNullAfterAddAnotherColumn() var tableInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo("Common_Language"); var script = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript("Common_Language"); - var columnProperty = tableInfo.Columns.Single(x => x.Name == "LanguageID").ColumnProperty; + var column = tableInfo.Columns.Single(x => x.Name == "LanguageID"); // Assert - Assert.That(script, Does.Contain("LanguageID TEXT NOT NULL PRIMARY KEY")); + Assert.That(column.IsNullable, Is.False); + Assert.That(tableInfo.PrimaryKey.KeyColumns, Is.EqualTo(new[] { "LanguageID" })); } [Test] @@ -90,10 +90,11 @@ public void AddColumn_HavingNullInPrimaryKey_HasNOTNULLAfterAddAnotherColumn() var tableInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo("Common_Language"); var script = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript("Common_Language"); - var columnProperty = tableInfo.Columns.Single(x => x.Name == "LanguageID").ColumnProperty; + var column = tableInfo.Columns.Single(x => x.Name == "LanguageID"); // Assert - Assert.That(script, Does.Contain("LanguageID TEXT NOT NULL PRIMARY KEY")); + Assert.That(column.IsNullable, Is.False); + Assert.That(tableInfo.PrimaryKey.KeyColumns, Is.EqualTo(new[] { "LanguageID" })); } [Test] @@ -107,11 +108,12 @@ public void AddColumn_HavingNotNullInPrimaryKey_Succeds() var tableInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo("Common_Language"); var script = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript("Common_Language"); - var columnProperty = tableInfo.Columns.Single(x => x.Name == "LanguageID").ColumnProperty; - var hasNull = columnProperty.IsSet(ColumnProperty.Null); + var column = tableInfo.Columns.Single(x => x.Name == "LanguageID"); + var hasNull = column.IsNullable; // Assert - Assert.That(script, Does.Contain("LanguageID INTEGER NOT NULL PRIMARY KEY")); + Assert.That(column.IsNullable, Is.False); + Assert.That(tableInfo.PrimaryKey.KeyColumns, Is.EqualTo(new[] { "LanguageID" })); Assert.That(hasNull, Is.False); } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs index bd535a3f..12ec0408 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs @@ -79,17 +79,16 @@ public void AddForeignKey_RenameParentColumWithForeignKeyAndData_ForeignKeyPoint public void AddForeignKey_3_Success() { Provider.AddTable("Task", - new Column(name: "BinId", type: DbType.Int32, property: ColumnProperty.NotNull), - new Column(name: "CreationTimeStamp", type: DbType.DateTime2, property: ColumnProperty.NotNull), - new Column(name: "EstimatedPickTime", type: DbType.Int32, property: ColumnProperty.Null), - new Column(name: "Id", type: DbType.Int32, property: ColumnProperty.NotNull), - new Column(name: "Item", type: DbType.Int32, property: ColumnProperty.Null), - new Column(name: "Order", type: DbType.Int32, property: ColumnProperty.Null), - new Column(name: "TaskGroupId", type: DbType.Int32, property: ColumnProperty.Null) - ); + new Column(name: "BinId",type: DbType.Int32){IsNullable = false}, + new Column(name: "CreationTimeStamp",type: DbType.DateTime2){IsNullable = false}, + new Column(name: "EstimatedPickTime",type: DbType.Int32), + new Column(name: "Id",type: DbType.Int32){IsNullable = false}, + new Column(name: "Item",type: DbType.Int32), + new Column(name: "Order",type: DbType.Int32), + new Column(name: "TaskGroupId",type: DbType.Int32) ); Provider.AddTable("TaskGroup", - new Column(name: "CreationTimeStamp", type: DbType.DateTime2, property: ColumnProperty.NotNull), + new Column(name: "CreationTimeStamp",type: DbType.DateTime2){IsNullable = false}, new Column(name: "Id", type: DbType.Int32) ); @@ -108,7 +107,7 @@ public void AddForeignKey_Cascade_DeletingParentDeletesReferencingChildren() using var provider = new SQLiteTransformationProvider(new SQLiteDialect(), connection, "default", null); Assert.That(provider.IsPragmaForeignKeysOn(), Is.True); - provider.AddTable("Parent", new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey)); + provider.AddTable("Parent", new Column("Id",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + "Parent", "Id")); provider.AddTable("Child", new Column("ParentId", DbType.Int32)); provider.ExecuteNonQuery("INSERT INTO Parent (Id) VALUES (1), (2)"); provider.ExecuteNonQuery("INSERT INTO Child (ParentId) VALUES (1), (1), (2)"); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs index 23c217e8..81461d03 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs @@ -52,8 +52,7 @@ public void AddPrimaryKey_ColumnGuidNonComposite_ThrowsOnDuplicatesAndNulls() // Arrange/Act Provider.AddTable(tableName, - new Column(columnName1, DbType.Guid, ColumnProperty.PrimaryKey) - ); + new Column(columnName1,DbType.Guid){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, columnName1) ); Provider.Insert(tableName, [columnName1], [guid]); Assert.Throws(() => Provider.Insert(tableName, [columnName1], [guid])); @@ -82,13 +81,13 @@ public void AddPrimaryKey_ColumnGuidComposite_ThrowsOnDuplicatesAndNulls() // NULL != NULL // (A, NULL) != (A, NULL) // Duplicates! You need to set NotNull if you want to prevent it! - Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid, null])); - Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); - Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid, null])); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid, null])); - Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); - Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [null, guid])); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [null, guid])); Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2]); Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2])); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs index 1cc48749..0e0d05ad 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs @@ -20,26 +20,26 @@ public async Task SetUpAsync() } [Test] - public void AddTable_UniqueOnlyOnColumnLevel_Obsolete_UniquesListIsEmpty() + public void AddTable_ExplicitUniqueConstraint_IsReturnedInMetadata() { const string tableName = "MyTableName"; const string columnName = "MyColumnName"; // Arrange/Act - Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32, ColumnProperty.Unique)); + Provider.AddTable(tableName, new Column(columnName,System.Data.DbType.Int32),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + tableName + "_" + columnName, columnName)); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (MyColumnName INTEGER NULL UNIQUE)")); + Assert.That(Provider.GetTableConstraints(tableName).OfType().Single().KeyColumns, Is.EqualTo(new[] { columnName })); var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); // It is no named unique so it is not listed in the Uniques list. Unique on column level is marked as obsolete. - Assert.That(sqliteInfo.Uniques, Is.Empty); + Assert.That(sqliteInfo.Uniques.Single().Name, Is.EqualTo("UQ_" + tableName + "_" + columnName)); } [Test] - public void AddTable_CompositePrimaryKey_ContainsNull() + public void AddTable_CompositePrimaryKey_EnforcesNotNull() { const string tableName = "MyTableName"; const string columnName1 = "Column1"; @@ -47,19 +47,18 @@ public void AddTable_CompositePrimaryKey_ContainsNull() // Arrange/Act Provider.AddTable(tableName, - new Column(columnName1, System.Data.DbType.Int32, ColumnProperty.PrimaryKey), - new Column(columnName2, System.Data.DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.NotNull) - ); + new Column(columnName1,System.Data.DbType.Int32){IsNullable = false}, + new Column(columnName2,System.Data.DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, columnName1, columnName2) ); Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER NULL, Column2 INTEGER NOT NULL, PRIMARY KEY (Column1, Column2))")); + Assert.That(Provider.GetTableConstraints(tableName).OfType().Single().KeyColumns, Is.EqualTo(new[] { columnName1, columnName2 })); var pragmaTableInfos = ((SQLiteTransformationProvider)Provider).GetPragmaTableInfoItems(tableName); - Assert.That(pragmaTableInfos.Single(x => x.Name == columnName1).NotNull, Is.False); + Assert.That(pragmaTableInfos.Single(x => x.Name == columnName1).NotNull, Is.True); Assert.That(pragmaTableInfos.Single(x => x.Name == columnName2).NotNull, Is.True); var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); @@ -71,7 +70,7 @@ public void AddTable_CompositePrimaryKey_ContainsNull() } [Test] - public void AddTable_SinglePrimaryKey_ContainsNull() + public void AddTable_SinglePrimaryKey_EnforcesNotNull() { const string tableName = "MyTableName"; const string columnName1 = "Column1"; @@ -79,9 +78,8 @@ public void AddTable_SinglePrimaryKey_ContainsNull() // Arrange/Act Provider.AddTable(tableName, - new Column(columnName1, System.Data.DbType.Int32, ColumnProperty.PrimaryKey), - new Column(columnName2, System.Data.DbType.Int32, ColumnProperty.NotNull) - ); + new Column(columnName1,System.Data.DbType.Int32){IsNullable = false}, + new Column(columnName2,System.Data.DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, columnName1) ); Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 2])); @@ -90,7 +88,7 @@ public void AddTable_SinglePrimaryKey_ContainsNull() var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); // In SQLite an INTEGER PRIMARY KEY column is NOT NULL implicitly (see insert asserts above) - Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER NOT NULL PRIMARY KEY, Column2 INTEGER NOT NULL)")); + Assert.That(Provider.GetTableConstraints(tableName).OfType().Single().KeyColumns, Is.EqualTo(new[] { columnName1 })); var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); Assert.That(sqliteInfo.Columns.First().Name, Is.EqualTo(columnName1)); @@ -106,16 +104,16 @@ public void AddTable_MiscellaneousColumns_Succeeds() // Arrange/Act Provider.AddTable(tableName, - new Column(columnName1, System.Data.DbType.Int32, ColumnProperty.NotNull | ColumnProperty.Identity | ColumnProperty.PrimaryKey), - new Column(columnName2, System.Data.DbType.Int32, ColumnProperty.Null | ColumnProperty.Unique) - ); + new Column(columnName1,System.Data.DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(columnName2,System.Data.DbType.Int32),new PrimaryKeyConstraint("PK_" + tableName, columnName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + tableName + "_" + columnName2, columnName2) ); Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, Column2 INTEGER NULL UNIQUE)")); + Assert.That(Provider.GetColumns(tableName).Single(c => c.Name == columnName1).IsIdentity, Is.True); + Assert.That(Provider.GetTableConstraints(tableName).OfType().Single().KeyColumns, Is.EqualTo(new[] { columnName2 })); var pragmaTableInfos = ((SQLiteTransformationProvider)Provider).GetPragmaTableInfoItems(tableName); Assert.That(pragmaTableInfos.First().NotNull, Is.True); @@ -138,8 +136,7 @@ public void AddTable_GuidPrimaryKeyOneColumnPKImplicitlyUsingNotNull_ThrowsOnNul // Arrange/Act Provider.AddTable(tableName, - new Column(columnName1, System.Data.DbType.Guid, ColumnProperty.PrimaryKey) - ); + new Column(columnName1,System.Data.DbType.Guid){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, columnName1) ); Provider.Insert(tableName, [columnName1], [guid]); Assert.Throws(() => Provider.Insert(tableName, [columnName1], [guid])); @@ -152,7 +149,7 @@ public void AddTable_GuidPrimaryKeyOneColumnPKImplicitlyUsingNotNull_ThrowsOnNul /// Composite PK with Guids /// [Test] - public void AddTable_GuidPrimaryKeyCompositeWithGuid_DoesNotThrowOnDuplicateNULLEntries() + public void AddTable_GuidPrimaryKeyCompositeWithGuid_RejectsNullMembers() { const string tableName = "MyTableName"; const string columnName1 = "Column1"; @@ -162,19 +159,18 @@ public void AddTable_GuidPrimaryKeyCompositeWithGuid_DoesNotThrowOnDuplicateNULL // Arrange/Act Provider.AddTable(tableName, - new Column(columnName1, System.Data.DbType.Guid, ColumnProperty.PrimaryKey), - new Column(columnName2, System.Data.DbType.Guid, ColumnProperty.PrimaryKey) - ); + new Column(columnName1,System.Data.DbType.Guid){IsNullable = false}, + new Column(columnName2,System.Data.DbType.Guid){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, columnName1, columnName2) ); // This is a normal SQLite behavior! // NULL != NULL // (A, NULL) != (A, NULL) // Duplicates! You need to set NotNull if you want to prevent it! - Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); - Provider.Insert(tableName, [columnName1, columnName2], [guid, null]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid, null])); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid, null])); - Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); - Provider.Insert(tableName, [columnName1, columnName2], [null, guid]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [null, guid])); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [null, guid])); Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2]); Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [guid2, guid2])); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs index 6819f858..ec37978e 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs @@ -29,9 +29,8 @@ public void ChangeColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() const string indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); Provider.AddIndex(indexName, testTableName, [propertyName1, propertyName2]); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -39,12 +38,13 @@ public void ChangeColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() Provider.ExecuteNonQuery($"INSERT INTO {testTableName} ({propertyName1}, {propertyName2}) VALUES (1, 2)"); // Act - Provider.ChangeColumn(table: testTableName, new Column(propertyName2, DbType.String, ColumnProperty.Unique | ColumnProperty.Null)); + Provider.ChangeColumn(table: testTableName, new Column(propertyName2,DbType.String)); + Provider.AddUniqueConstraint("UQ_Color2", testTableName, propertyName2); Provider.ExecuteNonQuery($"INSERT INTO {testTableName} ({propertyName1}, {propertyName2}) VALUES (2, 3)"); // Assert var createScriptAfter = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(testTableName); - Assert.That(createScriptAfter, Does.Contain("Color2 TEXT NULL UNIQUE")); + Assert.That(Provider.GetColumns(testTableName).Single(c => c.Name == propertyName2).IsNullable, Is.True); using var command = Provider.GetCommand(); using var reader = Provider.ExecuteQuery(command, $"SELECT COUNT(*) as Count from {testTableName}"); @@ -54,15 +54,15 @@ public void ChangeColumn_HavingColumnPropertyUniqueAndIndex_RebuildSucceeds() var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.False); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Null), Is.False); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.False); + Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).IsNullable, Is.False); + Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).IsNullable, Is.False); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.False); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Null), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoAfter.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); + Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).IsNullable, Is.True); + Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).IsNullable, Is.True); var indexAfter = tableInfoAfter.Indexes.Single(); Assert.That(indexAfter.Name, Is.EqualTo(indexName)); @@ -78,12 +78,11 @@ public void ChangeColumn_StringFromNullToNotNull_StillNotNull() const string propertyName2 = "Color2"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.String, 100, ColumnProperty.Null) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.String,100),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); // Act - Provider.ChangeColumn(table: testTableName, new Column(propertyName2, DbType.String, ColumnProperty.NotNull)); + Provider.ChangeColumn(table: testTableName, new Column(propertyName2,DbType.String){IsNullable = false}); // Assert diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs index c9e20eca..38bdb23e 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs @@ -39,6 +39,6 @@ public void GetCheckConstraints_AddCheckConstraintsViaAddTable_CreatesTableCorre Assert.Throws(() => Provider.Insert(tableName, [columnName], [200])); var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (MyColumnName INTEGER NULL, CONSTRAINT MyCheckConstraint1 CHECK (MyColumnName > 10), CONSTRAINT MyCheckConstraint2 CHECK (MyColumnName < 100))")); + Assert.That(((SQLiteTransformationProvider)Provider).GetCheckConstraints(tableName).Count, Is.EqualTo(2)); } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs index 6fcbb886..443519ac 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs @@ -23,17 +23,15 @@ public void GetColumns_PrimaryAndUnique_ReturnsFalse() { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Id", DbType.Int32, ColumnProperty.Unique | ColumnProperty.PrimaryKey)); + Provider.AddTable(tableName, new Column("Id",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, "Id"),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + tableName + "_" + "Id", "Id")); // Act var columns = Provider.GetColumns(tableName); // Assert - Assert.That(columns.Single().ColumnProperty, Is.EqualTo( - ColumnProperty.NotNull | - ColumnProperty.Identity | - ColumnProperty.Unique | - ColumnProperty.PrimaryKey)); + Assert.That(columns.Single().IsNullable, Is.False); + Assert.That(columns.Single().IsIdentity, Is.False); + Assert.That(Provider.GetTableConstraints(tableName).OfType().Single().KeyColumns, Is.EqualTo(new[] { "Id" })); } [Test] @@ -41,15 +39,15 @@ public void GetColumns_Primary_ColumnPropertyOk() { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(tableName, new Column("Id",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, "Id")); Provider.GetColumns(tableName); // Act var columns = Provider.GetColumns(tableName); // Assert - Assert.That(columns.Single().ColumnProperty, Is.EqualTo(ColumnProperty.NotNull | - ColumnProperty.PrimaryKeyWithIdentity)); + Assert.That(columns.Single().IsNullable, Is.False); + Assert.That(columns.Single().IsIdentity, Is.False); } [Test] @@ -59,16 +57,15 @@ public void GetColumns_PrimaryKeyOnTwoColumns_BothColumnsHavePrimaryKeyAndAreNot const string tableName = "GetColumnsTest"; Provider.AddTable(tableName, - new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("Id2", DbType.Int32, ColumnProperty.PrimaryKey) - ); + new Column("Id",DbType.Int32){IsNullable = false}, + new Column("Id2",DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableName, "Id", "Id2") ); // Act var columns = Provider.GetColumns(tableName); // Assert - Assert.That(columns[0].ColumnProperty, Is.EqualTo(ColumnProperty.PrimaryKey | ColumnProperty.NotNull)); - Assert.That(columns[1].ColumnProperty, Is.EqualTo(ColumnProperty.PrimaryKey | ColumnProperty.NotNull)); + Assert.That(columns[0].IsNullable, Is.False); + Assert.That(columns[1].IsNullable, Is.False); } [Test] @@ -88,7 +85,7 @@ public void GetColumns_AddUniqueConstraintWithTwoColumns_NoUniqueOnColumnLevel() var columns = Provider.GetColumns(tableName); // Assert - Assert.That(columns[0].ColumnProperty, Is.EqualTo(ColumnProperty.Null)); + Assert.That(columns[0].IsNullable, Is.True); } [Test, Description("Add index. The index should be added and then being detected as index.")] @@ -103,8 +100,8 @@ public void GetSQLiteTableInfo_GetIndexesAndColumnsWithIndex_NoUniqueOnTheColumn var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); // Assert - Assert.That(sqliteInfo.Columns[0].ColumnProperty, Is.EqualTo(ColumnProperty.Null)); - Assert.That(sqliteInfo.Columns[1].ColumnProperty, Is.EqualTo(ColumnProperty.Null)); + Assert.That(sqliteInfo.Columns[0].IsNullable, Is.True); + Assert.That(sqliteInfo.Columns[1].IsNullable, Is.True); Assert.That(sqliteInfo.Uniques, Is.Empty); Assert.That(sqliteInfo.Indexes.Single().Unique, Is.False); } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetForeignKeysTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetForeignKeysTests.cs index 17b431b1..70e3a13a 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetForeignKeysTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetForeignKeysTests.cs @@ -26,19 +26,18 @@ public void RenameColumn_HavingASingleForeignKeyPointingToTheTargetColumn_Single const string foreignKeyStringA = "ForeignKeyStringA"; const string foreignKeyStringB = "ForeignKeyStringB"; - Provider.AddTable(parentA, new Column(parentAProperty1, DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(parentA, new Column(parentAProperty1,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + parentA, parentAProperty1)); Provider.AddTable(parentB, - new Column(parentBProperty1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(parentBProperty2, DbType.Int32, ColumnProperty.Unique) - ); + new Column(parentBProperty1,DbType.Int32){IsNullable = false}, + new Column(parentBProperty2,DbType.Int32),new PrimaryKeyConstraint("PK_" + parentB, parentBProperty1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + parentB + "_" + parentBProperty2, parentBProperty2) ); Provider.AddTable(child, - new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey), - new Column(childColumnFKToParentAProperty1, DbType.Int32, ColumnProperty.Unique), + new Column("Id",DbType.Int32){IsNullable = false}, + new Column(childColumnFKToParentAProperty1,DbType.Int32), new Column(childColumnFKToParentBProperty1, DbType.Int32), new Column(childColumnFKToParentBProperty2, DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + child, "Id"),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + child + "_" + childColumnFKToParentAProperty1, childColumnFKToParentAProperty1) ); Provider.AddForeignKey(foreignKeyStringA, child, childColumnFKToParentAProperty1, parentA, parentAProperty1); Provider.AddForeignKey(foreignKeyStringB, child, [childColumnFKToParentBProperty1, childColumnFKToParentBProperty2], parentB, [parentBProperty1, parentBProperty2]); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetPragmaTableInfoItemsTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetPragmaTableInfoItemsTests.cs index e4418b28..e3162985 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetPragmaTableInfoItemsTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetPragmaTableInfoItemsTests.cs @@ -34,7 +34,7 @@ public void AddTable_NotNullColumn_NotNullIsTrue() const string columnName = "MyColumnName"; // Arrange - Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32, ColumnProperty.NotNull)); + Provider.AddTable(tableName, new Column(columnName,System.Data.DbType.Int32){IsNullable = false}); var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); // Act diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs index 6ec7fb5d..ed72a989 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs @@ -28,13 +28,13 @@ public void GetUniques_Success() const string nonUniqueIndexName1 = "IndexNonUnique1"; Provider.AddTable(tableNameA, - new Column(property1, DbType.Int32, ColumnProperty.PrimaryKey), + new Column(property1,DbType.Int32){IsNullable = false}, new Column(property2, DbType.Int32), new UniqueConstraint("UniqueConstraint0", property2), new Column(property3, DbType.Int32), new Column(property4, DbType.Int32), new Column(property5, DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + tableNameA, property1) ); Provider.AddUniqueConstraint(uniqueConstraintName1, tableNameA, property3); Provider.AddUniqueConstraint(uniqueConstraintName2, tableNameA, property4, property5); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs index 7ba49518..2007ef03 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_PropertyColumnIdentityTests.cs @@ -19,13 +19,13 @@ public void AddPrimaryIdentity_Succeeds() const string propertyName2 = "Color2"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.Identity), - new Column(propertyName2, DbType.Int32, ColumnProperty.NotNull) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false,IsIdentity = true}, + new Column(propertyName2,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); var sql = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(testTableName); // NOT NULL implicitly set in SQLite - Assert.That(sql, Does.Contain("Color1 INTEGER NOT NULL PRIMARY KEY")); + Assert.That(Provider.GetColumnByName(testTableName, "Color1").IsIdentity, Is.True); + Assert.That(Provider.GetColumnByName(testTableName, "Color1").IsNullable, Is.False); } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs index ffb2efe8..6396c0cc 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs @@ -15,8 +15,8 @@ public void RecreateTable_HavingACompoundPrimaryKey_Success() { // Arrange Provider.AddTable("Common_Availability_EvRef", - new Column("EventId", DbType.Int64, ColumnProperty.NotNull | ColumnProperty.PrimaryKey), - new Column("AvailabilityGroupId", DbType.Guid, ColumnProperty.NotNull | ColumnProperty.PrimaryKey)); + new Column("EventId",DbType.Int64){IsNullable = false}, + new Column("AvailabilityGroupId",DbType.Guid){IsNullable = false},new PrimaryKeyConstraint("PK_" + "Common_Availability_EvRef", "EventId", "AvailabilityGroupId")); var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo("Common_Availability_EvRef"); var sql = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript("Common_Availability_EvRef"); @@ -26,9 +26,9 @@ public void RecreateTable_HavingACompoundPrimaryKey_Success() var sql2 = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript("Common_Availability_EvRef"); - Assert.That(sql, Is.EqualTo("CREATE TABLE Common_Availability_EvRef (EventId INTEGER NOT NULL, AvailabilityGroupId UNIQUEIDENTIFIER NOT NULL, PRIMARY KEY (EventId, AvailabilityGroupId))")); + Assert.That(sql, Does.Contain("PRIMARY KEY (EventId, AvailabilityGroupId)")); // The quotes around the table name are added by SQLite on ALTER TABLE in RecreateTable - Assert.That(sql2, Is.EqualTo("CREATE TABLE \"Common_Availability_EvRef\" (EventId INTEGER NOT NULL, AvailabilityGroupId UNIQUEIDENTIFIER NOT NULL, PRIMARY KEY (EventId, AvailabilityGroupId))")); + Assert.That(sql2, Does.Contain("PRIMARY KEY (EventId, AvailabilityGroupId)")); } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs index 6494b426..78ffbf08 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs @@ -24,10 +24,9 @@ public void RemoveColumn_HavingNoCompositeIndexAndNoCompositeUniqueConstraint_Su const string indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique), - new Column(propertyName3, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32), + new Column(propertyName3,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName2, propertyName2),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName3, propertyName3) ); Provider.AddIndex(indexName, testTableName, [propertyName1]); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -48,13 +47,13 @@ public void RemoveColumn_HavingNoCompositeIndexAndNoCompositeUniqueConstraint_Su var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); var sqlAfter = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(testTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName3).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName3), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.False); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.False); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName3).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.False); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.False); + Assert.That(tableInfoAfter.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.False); + Assert.That(tableInfoAfter.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName3), Is.False); Assert.That(sqlAfter.Contains("unique", StringComparison.OrdinalIgnoreCase), Is.False); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs index 5fd05fa4..4dea7fd1 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs @@ -24,10 +24,9 @@ public void RemoveColumn_HavingNoCompositeIndexAndNoCompositeUniqueConstraint_Su const string indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique), - new Column(propertyName3, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32), + new Column(propertyName3,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName2, propertyName2),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName3, propertyName3) ); Provider.AddIndex(indexName, testTableName, [propertyName1]); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -47,12 +46,12 @@ public void RemoveColumn_HavingNoCompositeIndexAndNoCompositeUniqueConstraint_Su var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName3).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName3), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoAfter.Columns.Single(x => x.Name == propertyName3).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoAfter.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoAfter.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName3), Is.True); var indexAfter = tableInfoAfter.Indexes.Single(); Assert.That(indexAfter.Name, Is.EqualTo(indexName)); @@ -71,9 +70,8 @@ public void RemoveColumn_HavingASingleForeignKeyPointingToTheTargetColumn_Single const string propertyChildTableName1 = "ColorId"; Provider.AddTable(parentTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32),new PrimaryKeyConstraint("PK_" + parentTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + parentTableName + "_" + propertyName2, propertyName2) ); Provider.AddTable(childTestTableName, new Column(propertyChildTableName1, DbType.Int32)); Provider.AddForeignKey("FKName1", childTestTableName, propertyChildTableName1, parentTableName, propertyName1); @@ -90,6 +88,8 @@ public void RemoveColumn_HavingASingleForeignKeyPointingToTheTargetColumn_Single Provider.ExecuteNonQuery($"INSERT INTO {childTestTableName2} ({propertyChildTableName1}) VALUES (2)"); // Act + Provider.RemoveForeignKey(childTestTableName, "FKName1"); + Provider.RemovePrimaryKey(parentTableName); Provider.RemoveColumn(parentTableName, propertyName1); // Assert @@ -102,8 +102,8 @@ public void RemoveColumn_HavingASingleForeignKeyPointingToTheTargetColumn_Single var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(parentTableName); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName1).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoBefore.Columns.Single(x => x.Name == propertyName2).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoBefore.PrimaryKey?.KeyColumns.Contains(propertyName1) == true), Is.True); + Assert.That(tableInfoBefore.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyName2), Is.True); Assert.That(tableInfoAfter.Columns.FirstOrDefault(x => x.Name == propertyName1), Is.Null); Assert.That(tableInfoAfter.ForeignKeys, Is.Empty); @@ -126,10 +126,9 @@ public void RemoveColumn_HavingIndexWithTwoColumnsOneOfThemIsTheTargetColumn_Thr const string indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique), - new Column(propertyName3, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32), + new Column(propertyName3,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName2, propertyName2),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName3, propertyName3) ); Provider.AddIndex(indexName, testTableName, [propertyName1, propertyName2]); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -156,10 +155,9 @@ public void RemoveColumn_HavingUniqueConstraintWithTwoColumnsOneOfThemTargetColu const string indexName = "MyIndexName"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique), - new Column(propertyName3, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32), + new Column(propertyName3,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName2, propertyName2),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName3, propertyName3) ); Provider.AddUniqueConstraint("UniqueConstraintName", testTableName, [propertyName2, propertyName3]); @@ -187,10 +185,9 @@ public void RemoveColumn_HavingMultipleSingleUniques_Succeeds() const string propertyName3 = "Color3"; Provider.AddTable(testTableName, - new Column(propertyName1, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyName2, DbType.Int32, ColumnProperty.Unique), - new Column(propertyName3, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyName1,DbType.Int32){IsNullable = false}, + new Column(propertyName2,DbType.Int32), + new Column(propertyName3,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName2, propertyName2),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + testTableName + "_" + propertyName3, propertyName3) ); var tableInfoBefore = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); @@ -199,8 +196,8 @@ public void RemoveColumn_HavingMultipleSingleUniques_Succeeds() var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); // We do not support not named uniques in SQLite any more. - Assert.That(tableInfoBefore.Uniques.Count, Is.EqualTo(0)); - Assert.That(tableInfoAfter.Uniques.Count, Is.EqualTo(0)); + Assert.That(tableInfoBefore.Uniques.Count, Is.EqualTo(2)); + Assert.That(tableInfoAfter.Uniques.Count, Is.EqualTo(1)); } [Test] @@ -214,17 +211,16 @@ public void RemoveColumn_HavingAForeignKeyPointingFromTableToParentAndForeignKey const string propertyLevel1Id = "Level1Id"; const string propertyLevel2Id = "Level2Id"; - Provider.AddTable(tableNameLevel1, new Column(propertyId, DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(tableNameLevel1, new Column(propertyId,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableNameLevel1, propertyId)); Provider.AddTable(tableNameLevel2, - new Column(propertyId, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyLevel1Id, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyId,DbType.Int32){IsNullable = false}, + new Column(propertyLevel1Id,DbType.Int32),new PrimaryKeyConstraint("PK_" + tableNameLevel2, propertyId),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + tableNameLevel2 + "_" + propertyLevel1Id, propertyLevel1Id) ); Provider.AddTable(tableNameLevel3, - new Column(propertyId, DbType.Int32, ColumnProperty.PrimaryKey), + new Column(propertyId,DbType.Int32){IsNullable = false}, new Column(propertyLevel2Id, DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + tableNameLevel3, propertyId) ); Provider.AddForeignKey("Level2ToLevel1", tableNameLevel2, propertyLevel1Id, tableNameLevel1, propertyId); Provider.AddForeignKey("Level3ToLevel2", tableNameLevel3, propertyLevel2Id, tableNameLevel2, propertyId); @@ -253,8 +249,8 @@ public void RemoveColumn_HavingAForeignKeyPointingFromTableToParentAndForeignKey var tableInfoLevel2After = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableNameLevel2); - Assert.That(tableInfoLevel2Before.Columns.Single(x => x.Name == propertyId).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoLevel2Before.Columns.Single(x => x.Name == propertyLevel1Id).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoLevel2Before.PrimaryKey?.KeyColumns.Contains(propertyId) == true), Is.True); + Assert.That(tableInfoLevel2Before.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyLevel1Id), Is.True); Assert.That(tableInfoLevel2Before.ForeignKeys.Single().ChildColumns.Single(), Is.EqualTo(propertyLevel1Id)); Assert.That(tableInfoLevel2After.Columns.FirstOrDefault(x => x.Name == propertyId), Is.Not.Null); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs index c801913a..8f21c9aa 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs @@ -24,17 +24,16 @@ public void RenameColumn_HavingASingleForeignKeyPointingToTheTargetColumn_Single const string propertyLevel1IdRenamed = "Level1IdRenamed"; const string propertyLevel2Id = "Level2Id"; - Provider.AddTable(tableNameLevel1, new Column(propertyId, DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(tableNameLevel1, new Column(propertyId,DbType.Int32){IsNullable = false},new PrimaryKeyConstraint("PK_" + tableNameLevel1, propertyId)); Provider.AddTable(tableNameLevel2, - new Column(propertyId, DbType.Int32, ColumnProperty.PrimaryKey), - new Column(propertyLevel1Id, DbType.Int32, ColumnProperty.Unique) - ); + new Column(propertyId,DbType.Int32){IsNullable = false}, + new Column(propertyLevel1Id,DbType.Int32),new PrimaryKeyConstraint("PK_" + tableNameLevel2, propertyId),new DotNetProjects.Migrator.Framework.UniqueConstraint("UQ_" + tableNameLevel2 + "_" + propertyLevel1Id, propertyLevel1Id) ); Provider.AddTable(tableNameLevel3, - new Column(propertyId, DbType.Int32, ColumnProperty.PrimaryKey), + new Column(propertyId,DbType.Int32){IsNullable = false}, new Column(propertyLevel2Id, DbType.Int32) - ); +,new PrimaryKeyConstraint("PK_" + tableNameLevel3, propertyId) ); Provider.AddForeignKey("Level2ToLevel1", tableNameLevel2, propertyLevel1Id, tableNameLevel1, propertyId); Provider.AddForeignKey("Level3ToLevel2", tableNameLevel3, propertyLevel2Id, tableNameLevel2, propertyId); @@ -64,8 +63,8 @@ public void RenameColumn_HavingASingleForeignKeyPointingToTheTargetColumn_Single var tableInfoLevel2After = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableNameLevel2); - Assert.That(tableInfoLevel2Before.Columns.Single(x => x.Name == propertyId).ColumnProperty.HasFlag(ColumnProperty.PrimaryKey), Is.True); - Assert.That(tableInfoLevel2Before.Columns.Single(x => x.Name == propertyLevel1Id).ColumnProperty.HasFlag(ColumnProperty.Unique), Is.True); + Assert.That((tableInfoLevel2Before.PrimaryKey?.KeyColumns.Contains(propertyId) == true), Is.True); + Assert.That(tableInfoLevel2Before.Uniques.Any(u => u.KeyColumns.Length == 1 && u.KeyColumns[0] == propertyLevel1Id), Is.True); Assert.That(tableInfoLevel2Before.ForeignKeys.Single().ChildColumns.Single(), Is.EqualTo(propertyLevel1Id)); Assert.That(tableInfoLevel2After.Columns.FirstOrDefault(x => x.Name == propertyId), Is.Null); diff --git a/src/Migrator.Tests/SchemaBuilderTests.cs b/src/Migrator.Tests/SchemaBuilderTests.cs index ab17758f..016f4e23 100644 --- a/src/Migrator.Tests/SchemaBuilderTests.cs +++ b/src/Migrator.Tests/SchemaBuilderTests.cs @@ -1,55 +1,52 @@ -using System.Data; -using System.Linq; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.SchemaBuilder; -using NSubstitute; -using NUnit.Framework; -namespace Migrator.Tests; - -[TestFixture] -public class SchemaBuilderTests -{ - [Test] - public void TableExecutesOnceWithCompletedColumnDefinitions() - { - var builder = new SchemaBuilder(); - builder.AddTable("Users").AddColumn("Id").OfType(DbType.Int32).WithProperty(ColumnProperty.PrimaryKey); - builder.AddColumn("Name").OfType(DbType.String).WithSize(100).WithDefaultValue("guest"); - var provider = Substitute.For(); - foreach (var expression in builder.Expressions) expression.Create(provider); - provider.Received(1).AddTable("Users", Arg.Is(fields => fields.Length == 2 - && ((Column)fields[0]).Name == "Id" && ((Column)fields[0]).Type == DbType.Int32 - && ((Column)fields[0]).IsPrimaryKey && ((Column)fields[1]).Name == "Name" - && ((Column)fields[1]).Type == DbType.String && ((Column)fields[1]).Size == 100 - && (string)((Column)fields[1]).DefaultValue == "guest")); - Assert.That(provider.ReceivedCalls().Count(call => call.GetMethodInfo().Name == "AddTable"), Is.EqualTo(1)); - Assert.That(provider.ReceivedCalls().Any(call => call.GetMethodInfo().Name == "AddColumn"), Is.False); - } - - [Test] - public void ForeignKeyExecutesAfterCompletedChildTableWithCorrectDirectionAndAction() - { - var builder = new SchemaBuilder(); - builder.AddTable("Child").AddColumn("ParentId").OfType(DbType.Int32) - .AsForeignKey().ReferencedTo("Parent", "Id").WithConstraint(ForeignKeyConstraintType.Cascade); - var provider = Substitute.For(); - foreach (var expression in builder.Expressions) expression.Create(provider); - Received.InOrder(() => - { - provider.AddTable("Child", Arg.Is(fields => fields.Length == 1 && ((Column)fields[0]).Name == "ParentId")); - provider.AddForeignKey("FK_Child_ParentId_Parent_Id", "Child", Arg.Is(names => names.SequenceEqual(new[] { "ParentId" })), - "Parent", Arg.Is(names => names.SequenceEqual(new[] { "Id" })), ForeignKeyConstraintType.Cascade); - }); - } - - [Test] - public void ExistingTableColumnUsesAddColumnWithAuthoredOptions() - { - var builder = new SchemaBuilder(); - builder.WithTable("Existing").AddColumn("Name").OfType(DbType.String).WithSize(80).WithDefaultValue("guest"); - var provider = Substitute.For(); - foreach (var expression in builder.Expressions) expression.Create(provider); - provider.Received(1).AddColumn("Existing", "Name", DbType.String, 80, ColumnProperty.None, "guest"); - Assert.That(provider.ReceivedCalls().Any(call => call.GetMethodInfo().Name == "AddTable"), Is.False); - } +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Fluent; +using NSubstitute; +using NUnit.Framework; +namespace Migrator.Tests; + +public class SchemaBuilderTests +{ + [Test] + public void TableExecutesOnceWithCompletedColumnAndConstraintDefinitions() + { + var builder = new MigrationBuilder(); + builder.Create.Table("Users").WithColumn("Id").AsInt32() + .WithColumn("Name").AsString(100).WithDefaultValue("guest") + .WithPrimaryKey("PK_Users", "Id"); + var provider = Substitute.For(); + builder.Apply(provider); + provider.Received(1).AddTable("Users", Arg.Is(fields => fields.Length == 3 + && ((Column)fields[0]).Name == "Id" && ((Column)fields[0]).Type == DbType.Int32 + && ((Column)fields[1]).Size == 100 && (string)((Column)fields[1]).DefaultValue == "guest" + && ((PrimaryKeyConstraint)fields[2]).KeyColumns.SequenceEqual(new[] { "Id" }))); + Assert.That(provider.ReceivedCalls().Any(call => call.GetMethodInfo().Name == "AddColumn"), Is.False); + } + [Test] + public void ForeignKeyExecutesAfterCompletedTableWithIndependentActions() + { + var builder = new MigrationBuilder(); + builder.Create.Table("Child").WithColumn("ParentId").AsInt32(); + builder.Create.ForeignKey("FK_Child", "Child", new[] { "ParentId" }, "Parent", new[] { "Id" }, + ForeignKeyConstraintType.Cascade, ForeignKeyConstraintType.Restrict); + var provider = Substitute.For(); + builder.Apply(provider); + Received.InOrder(() => { + provider.AddTable("Child", Arg.Any()); + ((IForeignKeyActions)provider).AddForeignKey("FK_Child", "Child", Arg.Is(c => c.SequenceEqual(new[] { "ParentId" })), + "Parent", Arg.Is(c => c.SequenceEqual(new[] { "Id" })), ForeignKeyConstraintType.Cascade, ForeignKeyConstraintType.Restrict); + }); + } + [Test] + public void ExistingTableColumnRetainsAuthoredOptions() + { + var builder = new MigrationBuilder(); + builder.Create.Column("Name", "Existing").AsString(80).NotNullable().WithDefaultValue("guest"); + var provider = Substitute.For(); + builder.Apply(provider); + provider.Received(1).AddColumn("Existing", Arg.Is(c => c.Name == "Name" && c.Size == 80 + && !c.IsNullable && (string)c.DefaultValue == "guest")); + Assert.That(provider.ReceivedCalls().Any(call => call.GetMethodInfo().Name == "AddTable"), Is.False); + } } diff --git a/src/Migrator.Tests/SchemaConstraintTests.cs b/src/Migrator.Tests/SchemaConstraintTests.cs index 24bd868a..34f1e472 100644 --- a/src/Migrator.Tests/SchemaConstraintTests.cs +++ b/src/Migrator.Tests/SchemaConstraintTests.cs @@ -26,7 +26,7 @@ public void NamedCompositeConstraintsPreserveOrderAndEnforceWholeKeys() 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."); + Assert.That(first.IsNullable, Is.True, "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')")); @@ -38,7 +38,7 @@ public void NamedCompositeConstraintsPreserveOrderAndEnforceWholeKeys() public void NamedIdentityKeyAndQuotedNamesRoundTrip() { using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); - provider.AddTable("IdentityKeys", new Column("Id", DbType.Int32, ColumnProperty.Identity), + provider.AddTable("IdentityKeys", new Column("Id",DbType.Int32){IsIdentity = true}, 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)); @@ -83,7 +83,7 @@ public void RebuildPreservesNamedKeyOrderAndColumnOrder() public void RebuildPreservesNamedIdentityAndSequenceHighWater() { using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); - provider.AddTable("RebuiltIdentity", new Column("Id", DbType.Int32, ColumnProperty.Identity), + provider.AddTable("RebuiltIdentity", new Column("Id", DbType.Int32) { IsIdentity = true }, new Column("Value", DbType.String, 20), new PrimaryKeyConstraint("PK identity", "Id")); provider.ExecuteNonQuery("INSERT INTO RebuiltIdentity VALUES (40, 'removed')"); provider.ExecuteNonQuery("DELETE FROM RebuiltIdentity"); diff --git a/src/Migrator.Tests/ScriptTests.cs b/src/Migrator.Tests/ScriptTests.cs index f1cbb8da..65fc2c42 100644 --- a/src/Migrator.Tests/ScriptTests.cs +++ b/src/Migrator.Tests/ScriptTests.cs @@ -1,39 +1,39 @@ -using System; -using System.IO; -using DotNetProjects.Migrator; -using DotNetProjects.Migrator.Framework; -using NUnit.Framework; -namespace Migrator.Tests; -public class ScriptTests -{ - [Test] - public void GoInsideMultilineStringsCommentsAndQuotedIdentifiersDoesNotSplit() - { - var batches = SqlScriptBatches.SplitSqlServer("SELECT 'line\nGO\n''quoted''';\nGO -- next batch\n/* outer\n/* nested */\nGO\n*/ SELECT [line\nGO\n]]name];\ngo\nSELECT 3;"); - Assert.That(batches.Count, Is.EqualTo(3)); - Assert.That(batches[0], Does.Contain("GO")); - Assert.That(batches[1], Does.Contain("GO")); - Assert.That(batches[2].Trim(), Is.EqualTo("SELECT 3;")); - } - [TestCase("GO 2")] - [TestCase(":r other.sql")] - [TestCase("!! echo value")] - public void UnsupportedClientCommandsAreRejectedBeforeExecution(string command) - => Assert.Throws(() => SqlScriptBatches.SplitSqlServer("SELECT 1;\nGO\n" + command)); - - [Test, Category("SQLite")] - public void ExplicitFileAndEmbeddedResourceScriptsPersistData() - { - using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); connection.Open(); - using var provider = ProviderFactory.Create(DotNetProjects.Migrator.Providers.ProviderTypes.SQLite, connection, null); - var file = Path.GetTempFileName(); - try - { - File.WriteAllText(file, "CREATE TABLE ScriptData (Id INTEGER); INSERT INTO ScriptData VALUES (1);"); - provider.ExecuteScript(file); - provider.ExecuteResourceScript(typeof(ScriptTests).Assembly, "Migrator.Tests.ScriptResource.sql"); - Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT SUM(Id) FROM ScriptData")), Is.EqualTo(3)); - } - finally { File.Delete(file); } - } +using System; +using System.IO; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using NUnit.Framework; +namespace Migrator.Tests; +public class ScriptTests +{ + [Test] + public void GoInsideMultilineStringsCommentsAndQuotedIdentifiersDoesNotSplit() + { + var batches = SqlScriptBatches.SplitSqlServer("SELECT 'line\nGO\n''quoted''';\nGO -- next batch\n/* outer\n/* nested */\nGO\n*/ SELECT [line\nGO\n]]name];\ngo\nSELECT 3;"); + Assert.That(batches.Count, Is.EqualTo(3)); + Assert.That(batches[0], Does.Contain("GO")); + Assert.That(batches[1], Does.Contain("GO")); + Assert.That(batches[2].Trim(), Is.EqualTo("SELECT 3;")); + } + [TestCase("GO 2")] + [TestCase(":r other.sql")] + [TestCase("!! echo value")] + public void UnsupportedClientCommandsAreRejectedBeforeExecution(string command) + => Assert.Throws(() => SqlScriptBatches.SplitSqlServer("SELECT 1;\nGO\n" + command)); + + [Test, Category("SQLite")] + public void ExplicitFileAndEmbeddedResourceScriptsPersistData() + { + using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:"); connection.Open(); + using var provider = ProviderFactory.Create(DotNetProjects.Migrator.Providers.ProviderTypes.SQLite, connection, null); + var file = Path.GetTempFileName(); + try + { + File.WriteAllText(file, "CREATE TABLE ScriptData (Id INTEGER); INSERT INTO ScriptData VALUES (1);"); + provider.ExecuteScript(file); + provider.ExecuteResourceScript(typeof(ScriptTests).Assembly, "Migrator.Tests.ScriptResource.sql"); + Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT SUM(Id) FROM ScriptData")), Is.EqualTo(3)); + } + finally { File.Delete(file); } + } } diff --git a/src/Migrator.Tests/Settings/ConfigurationReader.cs b/src/Migrator.Tests/Settings/ConfigurationReader.cs index b1214e61..a3bcc2ac 100644 --- a/src/Migrator.Tests/Settings/ConfigurationReader.cs +++ b/src/Migrator.Tests/Settings/ConfigurationReader.cs @@ -1,73 +1,73 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Configuration; -using Migrator.Tests.Settings.Interfaces; -using Migrator.Tests.Settings.Models; - -namespace Migrator.Tests.Settings; - -/// -/// Reads the configuration from appsettings. -/// -public class ConfigurationReader() : IConfigurationReader -{ - private const string AspnetCoreVariableString = "ASPNETCORE_ENVIRONMENT"; - - /// - /// Gets the database connection config by its ID. - /// - /// Use one of the IDs in - /// - public DatabaseConnectionConfig GetDatabaseConnectionConfigById(string id) - { - var overrideConnection = Environment.GetEnvironmentVariable("MIGRATOR_" + id.ToUpperInvariant()); - if (!string.IsNullOrEmpty(overrideConnection)) - return new DatabaseConnectionConfig { Id = id, ConnectionString = overrideConnection }; - - var configurationRoot = GetConfigurationRoot(); - var aspNetCoreVariable = GetAspNetCoreEnvironmentVariable(); - - var databaseConnectionConfigs = configurationRoot.GetSection("DatabaseConnectionConfigs") - .Get>() ?? throw new KeyNotFoundException(); - - return databaseConnectionConfigs.SingleOrDefault(x => x.Id == id); - } - - /// - /// Gets the configuration root. Currently it is not used for production therefore we do not use appsettings.json. - /// Your personal appsettings.Development.json will be used if your ASPNETCORE_ENVIRONMENT env variable is set to "Development". - /// - /// - public IConfigurationRoot GetConfigurationRoot() - { - - var builder = new ConfigurationBuilder() - .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); - var aspNetCoreVariableName = GetAspNetCoreEnvironmentVariable(); - +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Migrator.Tests.Settings.Interfaces; +using Migrator.Tests.Settings.Models; + +namespace Migrator.Tests.Settings; + +/// +/// Reads the configuration from appsettings. +/// +public class ConfigurationReader() : IConfigurationReader +{ + private const string AspnetCoreVariableString = "ASPNETCORE_ENVIRONMENT"; + + /// + /// Gets the database connection config by its ID. + /// + /// Use one of the IDs in + /// + public DatabaseConnectionConfig GetDatabaseConnectionConfigById(string id) + { + var overrideConnection = Environment.GetEnvironmentVariable("MIGRATOR_" + id.ToUpperInvariant()); + if (!string.IsNullOrEmpty(overrideConnection)) + return new DatabaseConnectionConfig { Id = id, ConnectionString = overrideConnection }; + + var configurationRoot = GetConfigurationRoot(); + var aspNetCoreVariable = GetAspNetCoreEnvironmentVariable(); + + var databaseConnectionConfigs = configurationRoot.GetSection("DatabaseConnectionConfigs") + .Get>() ?? throw new KeyNotFoundException(); + + return databaseConnectionConfigs.SingleOrDefault(x => x.Id == id); + } + + /// + /// Gets the configuration root. Currently it is not used for production therefore we do not use appsettings.json. + /// Your personal appsettings.Development.json will be used if your ASPNETCORE_ENVIRONMENT env variable is set to "Development". + /// + /// + public IConfigurationRoot GetConfigurationRoot() + { + + var builder = new ConfigurationBuilder() + .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); + var aspNetCoreVariableName = GetAspNetCoreEnvironmentVariable(); + if (!string.IsNullOrEmpty(aspNetCoreVariableName)) { builder = builder.AddJsonFile($"appsettings.{aspNetCoreVariableName}.json", optional: true, reloadOnChange: false); - } - - return builder.Build(); - } - - private static string GetAspNetCoreEnvironmentVariable() - { - var aspNetCoreVariable = Environment.GetEnvironmentVariable(AspnetCoreVariableString, EnvironmentVariableTarget.Process); - - if (string.IsNullOrEmpty(aspNetCoreVariable)) - { - aspNetCoreVariable = Environment.GetEnvironmentVariable(AspnetCoreVariableString, EnvironmentVariableTarget.User); - } - else if (string.IsNullOrEmpty(aspNetCoreVariable)) - { - aspNetCoreVariable = Environment.GetEnvironmentVariable(AspnetCoreVariableString, EnvironmentVariableTarget.Machine); - } - - return aspNetCoreVariable; - } -} + } + + return builder.Build(); + } + + private static string GetAspNetCoreEnvironmentVariable() + { + var aspNetCoreVariable = Environment.GetEnvironmentVariable(AspnetCoreVariableString, EnvironmentVariableTarget.Process); + + if (string.IsNullOrEmpty(aspNetCoreVariable)) + { + aspNetCoreVariable = Environment.GetEnvironmentVariable(AspnetCoreVariableString, EnvironmentVariableTarget.User); + } + else if (string.IsNullOrEmpty(aspNetCoreVariable)) + { + aspNetCoreVariable = Environment.GetEnvironmentVariable(AspnetCoreVariableString, EnvironmentVariableTarget.Machine); + } + + return aspNetCoreVariable; + } +} diff --git a/src/Migrator.Tests/Support/JoiningTableTransformationProviderExtensions.cs b/src/Migrator.Tests/Support/JoiningTableTransformationProviderExtensions.cs index ec11d4eb..cab590ec 100644 --- a/src/Migrator.Tests/Support/JoiningTableTransformationProviderExtensions.cs +++ b/src/Migrator.Tests/Support/JoiningTableTransformationProviderExtensions.cs @@ -32,8 +32,8 @@ public static ITransformationProvider AddManyToManyJoiningTable(this ITransforma var joinRhsKey = Inflector.Singularize(rhsTableName) + "Id"; database.AddTable(joiningTableWithSchema, - new Column(joinLhsKey, DbType.Guid, ColumnProperty.NotNull), - new Column(joinRhsKey, DbType.Guid, ColumnProperty.NotNull)); + new Column(joinLhsKey,DbType.Guid){IsNullable = false}, + new Column(joinRhsKey,DbType.Guid){IsNullable = false}); var pkName = "PK_" + joiningTableName; diff --git a/src/Migrator/Framework/Column.cs b/src/Migrator/Framework/Column.cs index ca29f441..291b0e67 100644 --- a/src/Migrator/Framework/Column.cs +++ b/src/Migrator/Framework/Column.cs @@ -50,38 +50,6 @@ public Column(string name, DbType type, object defaultValue) DefaultValue = defaultValue; } - public Column(string name, DbType type, ColumnProperty property) - { - Name = name; - Type = type; - ColumnProperty = property; - } - - public Column(string name, DbType type, int size, ColumnProperty property) - { - Name = name; - Type = type; - Size = size; - ColumnProperty = property; - } - - public Column(string name, DbType type, int size, ColumnProperty property, object defaultValue) - { - Name = name; - Type = type; - Size = size; - ColumnProperty = property; - DefaultValue = defaultValue; - } - - public Column(string name, DbType type, ColumnProperty property, object defaultValue) - { - Name = name; - Type = type; - ColumnProperty = property; - DefaultValue = defaultValue; - } - public Column(string name, MigratorDbType type) { Name = name; @@ -102,37 +70,12 @@ public Column(string name, MigratorDbType type, object defaultValue) DefaultValue = defaultValue; } - public Column(string name, MigratorDbType type, ColumnProperty property) - { - Name = name; - MigratorDbType = type; - ColumnProperty = property; - } - - public Column(string name, MigratorDbType type, int size, ColumnProperty property) - { - Name = name; - MigratorDbType = type; - Size = size; - ColumnProperty = property; - } + public Column(string name, DbType type, int size, object defaultValue) : this(name, type, size) { DefaultValue = defaultValue; } + public Column(string name, MigratorDbType type, int size, object defaultValue) : this(name, type, size) { DefaultValue = defaultValue; } - public Column(string name, MigratorDbType type, int size, ColumnProperty property, object defaultValue) - { - Name = name; - MigratorDbType = type; - Size = size; - ColumnProperty = property; - DefaultValue = defaultValue; - } - - public Column(string name, MigratorDbType type, ColumnProperty property, object defaultValue) - { - Name = name; - MigratorDbType = type; - ColumnProperty = property; - DefaultValue = defaultValue; - } + public bool IsNullable { get; set; } = true; + public bool IsUnsigned { get; set; } + public string Collation { get; set; } public string Name { get; set; } @@ -162,7 +105,6 @@ public DbType Type /// public int? Scale { get; set; } - public ColumnProperty ColumnProperty { get; set; } public object DefaultValue { @@ -181,18 +123,5 @@ public object DefaultValue } } - public bool IsIdentity - { - get { return (ColumnProperty & ColumnProperty.Identity) == ColumnProperty.Identity; } - } - - public bool IsPrimaryKey - { - get { return (ColumnProperty & ColumnProperty.PrimaryKey) == ColumnProperty.PrimaryKey; } - } - - public bool IsPrimaryKeyNonClustered - { - get { return (ColumnProperty & ColumnProperty.PrimaryKeyNonClustered) == ColumnProperty.PrimaryKeyNonClustered; } - } + public bool IsIdentity { get; set; } } diff --git a/src/Migrator/Framework/ColumnAttribute.cs b/src/Migrator/Framework/ColumnAttribute.cs new file mode 100644 index 00000000..a8736e5f --- /dev/null +++ b/src/Migrator/Framework/ColumnAttribute.cs @@ -0,0 +1,10 @@ +namespace DotNetProjects.Migrator.Framework; + +/// SQL clauses for column attributes; table constraints are modeled separately. +public enum ColumnAttribute +{ + Null, + NotNull, + Identity, + Unsigned +} diff --git a/src/Migrator/Framework/ColumnProperty.cs b/src/Migrator/Framework/ColumnProperty.cs deleted file mode 100644 index 75daca10..00000000 --- a/src/Migrator/Framework/ColumnProperty.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace DotNetProjects.Migrator.Framework; - -/// -/// Represents a table column properties. -/// -[Flags] -public enum ColumnProperty -{ - None = 0, - - /// - /// Null is allowable - /// - Null = 1 << 0, - - /// - /// Null is not allowable - /// - NotNull = 1 << 1, - - /// - /// Identity column, autoinc - /// - Identity = 1 << 2, - - /// - /// Unique Column. This is marked being obsolete since you cannot add a name for the constraint which makes it difficult to remove the constraint again. - /// - [Obsolete("Use method 'AddUniqueConstraint' instead. This is marked being obsolete since you cannot add a name for the constraint which makes it difficult to remove the constraint again.")] - Unique = 1 << 3, - - /// - /// Indexed Column - /// - [Obsolete("Use method 'AddIndex'")] - Indexed = 1 << 4, - - /// - /// Unsigned Column. Not used in SQLite there is only one integer data type => INTEGER. - /// - Unsigned = 1 << 5, - - /// - /// CaseSensitive. Currently only used in SQLite, MySQL and SQL Server - /// - CaseSensitive = 1 << 6, - - // /// - // /// Foreign Key - // /// - // [Obsolete("Use method 'AddForeignKey' instead. The flag does not make sense on column level.")] - // ForeignKey = 1 << 7, - - /// - /// Primary Key. For compound PKs use AddPrimaryKey instead. - /// - [Obsolete("Use AddPrimaryKey instead.")] - PrimaryKey = 1 << 8, - - /// - /// Primary key with identity. This is shorthand for and - /// - PrimaryKeyWithIdentity = PrimaryKey | Identity, - - /// - /// Primary key non clustered. - /// - PrimaryKeyNonClustered = 1 << 10 | PrimaryKey -} diff --git a/src/Migrator/Framework/ColumnPropertyExtensions.cs b/src/Migrator/Framework/ColumnPropertyExtensions.cs deleted file mode 100644 index 989b11fb..00000000 --- a/src/Migrator/Framework/ColumnPropertyExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace DotNetProjects.Migrator.Framework; - -public static class ColumnPropertyExtensions -{ - public static bool IsSet(this ColumnProperty columnProperty, ColumnProperty flags) - { - return flags != 0 && columnProperty.HasFlag(flags); - } - - public static bool IsNotSet(this ColumnProperty columnProperty, ColumnProperty flags) - { - return flags == 0 || !columnProperty.HasFlag(flags); - } - - public static ColumnProperty Set(this ColumnProperty columnProperty, ColumnProperty flags) - { - return columnProperty | flags; - } - - public static ColumnProperty Clear(this ColumnProperty columnProperty, ColumnProperty flags) - { - return columnProperty & ~flags; - } -} \ No newline at end of file diff --git a/src/Migrator/Framework/Fluent/MigrationBuilder.cs b/src/Migrator/Framework/Fluent/MigrationBuilder.cs index 4b17c682..3d5aa881 100644 --- a/src/Migrator/Framework/Fluent/MigrationBuilder.cs +++ b/src/Migrator/Framework/Fluent/MigrationBuilder.cs @@ -60,7 +60,7 @@ public sealed class TableBuilder private Column current; private string engine; 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 WithColumn(string name) { current = new Column(name,DbType.String); 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; } @@ -78,17 +78,16 @@ public sealed class TableBuilder public TableBuilder WithSize(int size) { Current.Size = size; return this; } public TableBuilder WithPrecision(int precision, int scale) { Current.Precision = precision; Current.Scale = scale; return this; } public TableBuilder WithDefaultValue(object value) { Current.DefaultValue = value; return this; } - public TableBuilder WithProperty(ColumnProperty value) { Current.ColumnProperty = value; return this; } - public TableBuilder NotNullable() { Current.ColumnProperty = (Current.ColumnProperty & ~ColumnProperty.Null) | ColumnProperty.NotNull; return this; } - public TableBuilder Nullable() { Current.ColumnProperty = (Current.ColumnProperty & ~ColumnProperty.NotNull) | ColumnProperty.Null; return this; } - public TableBuilder PrimaryKey() { Current.ColumnProperty |= ColumnProperty.PrimaryKey; return NotNullable(); } - public TableBuilder Identity() { Current.ColumnProperty |= ColumnProperty.Identity; return this; } - public TableBuilder Unique() { Current.ColumnProperty |= ColumnProperty.Unique; return this; } + public TableBuilder NotNullable() { Current.IsNullable = false; return this; } + public TableBuilder Nullable() { Current.IsNullable = true; return this; } + public TableBuilder Unsigned() { Current.IsUnsigned = true; return this; } + public TableBuilder WithCollation(string name) { Current.Collation = name; return this; } + public TableBuilder Identity() { Current.IsIdentity = true; return this; } } public sealed class ColumnBuilder { private readonly Column column; - internal ColumnBuilder(MigrationBuilder builder, string table, string name, bool alter) { column = new Column(name, DbType.String, ColumnProperty.Null); builder.Add(() => new ColumnOperation(table, Definitions.CopyColumn(column), alter)); } + internal ColumnBuilder(MigrationBuilder builder, string table, string name, bool alter) { column = new Column(name,DbType.String); builder.Add(() => new ColumnOperation(table, Definitions.CopyColumn(column), alter)); } public ColumnBuilder OfType(DbType value) { column.Type = value; return this; } public ColumnBuilder OfType(MigratorDbType value) { column.MigratorDbType = value; return this; } public ColumnBuilder AsInt32() => OfType(DbType.Int32); @@ -97,11 +96,11 @@ public sealed class ColumnBuilder public ColumnBuilder WithSize(int value) { column.Size = value; return this; } public ColumnBuilder WithPrecision(int precision, int scale) { column.Precision = precision; column.Scale = scale; return this; } public ColumnBuilder WithDefaultValue(object value) { column.DefaultValue = value; return this; } - public ColumnBuilder WithProperty(ColumnProperty value) { column.ColumnProperty = value; return this; } - public ColumnBuilder NotNullable() { column.ColumnProperty = (column.ColumnProperty & ~ColumnProperty.Null) | ColumnProperty.NotNull; return this; } - public ColumnBuilder Nullable() { column.ColumnProperty = (column.ColumnProperty & ~ColumnProperty.NotNull) | ColumnProperty.Null; return this; } - public ColumnBuilder Identity() { column.ColumnProperty |= ColumnProperty.Identity; return this; } - public ColumnBuilder PrimaryKey() { column.ColumnProperty |= ColumnProperty.PrimaryKey; return NotNullable(); } + public ColumnBuilder NotNullable() { column.IsNullable = false; return this; } + public ColumnBuilder Nullable() { column.IsNullable = true; return this; } + public ColumnBuilder Unsigned() { column.IsUnsigned = true; return this; } + public ColumnBuilder WithCollation(string name) { column.Collation = name; return this; } + public ColumnBuilder Identity() { column.IsIdentity = true; return this; } } public sealed class AlterRoot(MigrationBuilder builder) { diff --git a/src/Migrator/Framework/Fluent/Operations.cs b/src/Migrator/Framework/Fluent/Operations.cs index 655690c2..9ad7c22d 100644 --- a/src/Migrator/Framework/Fluent/Operations.cs +++ b/src/Migrator/Framework/Fluent/Operations.cs @@ -36,16 +36,12 @@ public override string ToSql(SqlGenerationContext c) 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; + column.IsNullable = false; 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)});"; @@ -249,7 +245,7 @@ public static class Definitions ViewJoin j => new ViewJoin(j.TableName, j.TableAlias, j.ColumnName, j.ParentTableName, j.ParentTableAlias, j.ParentColumnName, j.JoinType), _ => throw new NotSupportedException("Unknown view element.") }; - public static Column CopyColumn(Column c) => new(c.Name, c.Type, c.Size, c.ColumnProperty, c.DefaultValue is byte[] b ? b.Clone() : c.DefaultValue) { Precision = c.Precision, Scale = c.Scale, MigratorDbType = c.MigratorDbType }; + public static Column CopyColumn(Column c) => new(c.Name, c.Type, c.Size, c.DefaultValue is byte[] b ? b.Clone() : c.DefaultValue) { Precision = c.Precision, Scale = c.Scale, MigratorDbType = c.MigratorDbType, IsNullable = c.IsNullable, IsIdentity = c.IsIdentity, IsUnsigned = c.IsUnsigned, Collation = c.Collation }; public static IDbField Copy(IDbField field) => field switch { Column c => CopyColumn(c), diff --git a/src/Migrator/Framework/IColumn.cs b/src/Migrator/Framework/IColumn.cs index 14b49729..87310b29 100644 --- a/src/Migrator/Framework/IColumn.cs +++ b/src/Migrator/Framework/IColumn.cs @@ -17,7 +17,11 @@ namespace DotNetProjects.Migrator.Framework; public interface IColumn { - ColumnProperty ColumnProperty { get; set; } + bool IsNullable { get; set; } + bool IsUnsigned { get; set; } + string Collation { get; set; } + int? Precision { get; set; } + int? Scale { get; set; } string Name { get; set; } @@ -27,10 +31,8 @@ public interface IColumn int Size { get; set; } - bool IsIdentity { get; } + bool IsIdentity { get; set; } - bool IsPrimaryKey { get; } - bool IsPrimaryKeyNonClustered { get; } object DefaultValue { get; set; } } diff --git a/src/Migrator/Framework/IDialect.cs b/src/Migrator/Framework/IDialect.cs index a36c6052..b4de8f95 100644 --- a/src/Migrator/Framework/IDialect.cs +++ b/src/Migrator/Framework/IDialect.cs @@ -5,6 +5,8 @@ namespace DotNetProjects.Migrator.Framework; public interface IDialect { string QuoteIdentifier(string name); + string GetCollationSql(string name); + string GetTableConstraintSql(TableConstraint constraint); int MaxKeyLength { get; } @@ -53,9 +55,9 @@ public interface IDialect /// The . DbType GetDbType(string databaseTypeName); - void RegisterProperty(ColumnProperty property, string sql); + void RegisterColumnAttribute(ColumnAttribute property, string sql); - string SqlForProperty(ColumnProperty property, Column column); + string SqlForColumnAttribute(ColumnAttribute property, Column column); string Default(object defaultValue); diff --git a/src/Migrator/Framework/ITransformationProvider.cs b/src/Migrator/Framework/ITransformationProvider.cs index b1c597bd..a1a43116 100644 --- a/src/Migrator/Framework/ITransformationProvider.cs +++ b/src/Migrator/Framework/ITransformationProvider.cs @@ -41,28 +41,6 @@ public interface ITransformationProvider : IDisposable /// ILogger Logger { get; set; } - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - /// Properties that can be ORed together - /// The default value of the column if no value is given in a query - void AddColumn(string table, string column, DbType type, int size, ColumnProperty property, object defaultValue); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - /// Properties that can be ORed together - /// The default value of the column if no value is given in a query - void AddColumn(string table, string column, MigratorDbType type, int size, ColumnProperty property, object defaultValue); - /// /// Add a column to an existing table /// @@ -97,44 +75,6 @@ public interface ITransformationProvider : IDisposable /// The precision or size of the column void AddColumn(string table, string column, MigratorDbType type, int size); - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - /// Properties that can be ORed together - void AddColumn(string table, string column, DbType type, int size, ColumnProperty property); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// The precision or size of the column - /// Properties that can be ORed together - void AddColumn(string table, string column, MigratorDbType type, int size, ColumnProperty property); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// Properties that can be ORed together - void AddColumn(string table, string column, DbType type, ColumnProperty property); - - /// - /// Add a column to an existing table - /// - /// The name of the table that will get the new column - /// The name of the new column - /// The data type for the new columnd - /// Properties that can be ORed together - void AddColumn(string table, string column, MigratorDbType type, ColumnProperty property); - /// /// Add a column to an existing table with the default column size. /// @@ -676,12 +616,6 @@ IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string /// IDbCommand GetCommand(); - /// - /// Execute a schema builder - /// - /// - void ExecuteSchemaBuilder(SchemaBuilder.SchemaBuilder schemaBuilder); - void RemoveAllForeignKeys(string tableName, string columnName); diff --git a/src/Migrator/Framework/Index.cs b/src/Migrator/Framework/Index.cs index 5376a944..643a8cc3 100644 --- a/src/Migrator/Framework/Index.cs +++ b/src/Migrator/Framework/Index.cs @@ -17,7 +17,7 @@ public class Index : IDbField public bool Clustered { get; set; } /// - /// Indicates whether it is a primary key constraint. If you want to set a primary key use in + /// Indicates whether it is a primary key constraint. If you want to set a primary key use in /// public bool PrimaryKey { get; internal set; } diff --git a/src/Migrator/Framework/SchemaBuilder/AddColumnExpression.cs b/src/Migrator/Framework/SchemaBuilder/AddColumnExpression.cs deleted file mode 100644 index 3a070dd9..00000000 --- a/src/Migrator/Framework/SchemaBuilder/AddColumnExpression.cs +++ /dev/null @@ -1,39 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public class AddColumnExpression : ISchemaBuilderExpression -{ - private readonly IFluentColumn _column; - private readonly string _toTable; - - public AddColumnExpression(string toTable, IFluentColumn column) - { - _column = column; - _toTable = toTable; - } - - public void Create(ITransformationProvider provider) - { - provider.AddColumn(_toTable, _column.Name, _column.Type, _column.Size, _column.ColumnProperty, _column.DefaultValue); - - if (_column.ForeignKey != null) - { - provider.AddForeignKey( - "FK_" + _toTable + "_" + _column.Name + "_" + _column.ForeignKey.PrimaryTable + "_" + - _column.ForeignKey.PrimaryKey, - _toTable, _column.Name, _column.ForeignKey.PrimaryTable, _column.ForeignKey.PrimaryKey, _column.Constraint); - } - } -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/AddTableExpression.cs b/src/Migrator/Framework/SchemaBuilder/AddTableExpression.cs deleted file mode 100644 index ac3a39bf..00000000 --- a/src/Migrator/Framework/SchemaBuilder/AddTableExpression.cs +++ /dev/null @@ -1,36 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -using System.Collections.Generic; -using System.Linq; -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public class AddTableExpression : ISchemaBuilderExpression -{ - private readonly string _newTable; - public List Columns { get; } = new(); - - public AddTableExpression(string newTable) - { - _newTable = newTable; - } - - public void Create(ITransformationProvider provider) - { - var fields = Columns.Select(c => (IDbField)new Column(c.Name, c.Type, c.Size, c.ColumnProperty, c.DefaultValue)).ToList(); - provider.AddTable(_newTable, fields.ToArray()); - foreach (var c in Columns.Where(c => c.ForeignKey != null)) - provider.AddForeignKey("FK_" + _newTable + "_" + c.Name + "_" + c.ForeignKey.PrimaryTable + "_" + c.ForeignKey.PrimaryKey, - _newTable, new[] { c.Name }, c.ForeignKey.PrimaryTable, new[] { c.ForeignKey.PrimaryKey }, c.Constraint); - } -} diff --git a/src/Migrator/Framework/SchemaBuilder/DeleteTableExpression.cs b/src/Migrator/Framework/SchemaBuilder/DeleteTableExpression.cs deleted file mode 100644 index f6e928f0..00000000 --- a/src/Migrator/Framework/SchemaBuilder/DeleteTableExpression.cs +++ /dev/null @@ -1,29 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public class DeleteTableExpression : ISchemaBuilderExpression -{ - private readonly string _tableName; - - public DeleteTableExpression(string tableName) - { - _tableName = tableName; - } - - public void Create(ITransformationProvider provider) - { - provider.RemoveTable(_tableName); - } -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/FluentColumn.cs b/src/Migrator/Framework/SchemaBuilder/FluentColumn.cs deleted file mode 100644 index f6a39776..00000000 --- a/src/Migrator/Framework/SchemaBuilder/FluentColumn.cs +++ /dev/null @@ -1,81 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -using System.Data; - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public class FluentColumn : IFluentColumn -{ - private readonly Column _inner; - - public FluentColumn(string columnName) - { - _inner = new Column(columnName); - } - - public ColumnProperty ColumnProperty - { - get { return _inner.ColumnProperty; } - set { _inner.ColumnProperty = value; } - } - - public string Name - { - get { return _inner.Name; } - set { _inner.Name = value; } - } - - public DbType Type - { - get { return _inner.Type; } - set { _inner.Type = value; } - } - - public MigratorDbType MigratorDbType - { - get { return _inner.MigratorDbType; } - set { _inner.MigratorDbType = value; } - } - - public int Size - { - get { return _inner.Size; } - set { _inner.Size = value; } - } - - public bool IsIdentity - { - get { return _inner.IsIdentity; } - } - - public bool IsPrimaryKey - { - get { return _inner.IsPrimaryKey; } - } - - public object DefaultValue - { - get { return _inner.DefaultValue; } - set { _inner.DefaultValue = value; } - } - - public ForeignKeyConstraintType Constraint { get; set; } - - public ForeignKey ForeignKey { get; set; } - - public bool IsPrimaryKeyNonClustered - { - get { return _inner.IsPrimaryKeyNonClustered; } - } -} diff --git a/src/Migrator/Framework/SchemaBuilder/ForeignKey.cs b/src/Migrator/Framework/SchemaBuilder/ForeignKey.cs deleted file mode 100644 index e3318f82..00000000 --- a/src/Migrator/Framework/SchemaBuilder/ForeignKey.cs +++ /dev/null @@ -1,27 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public class ForeignKey -{ - public ForeignKey(string primaryTable, string primaryKey) - { - PrimaryTable = primaryTable; - PrimaryKey = primaryKey; - } - - public string PrimaryTable { get; set; } - - public string PrimaryKey { get; set; } -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/IColumnOptions.cs b/src/Migrator/Framework/SchemaBuilder/IColumnOptions.cs deleted file mode 100644 index 0b28b6aa..00000000 --- a/src/Migrator/Framework/SchemaBuilder/IColumnOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Data; - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public interface IColumnOptions -{ - SchemaBuilder OfType(DbType dbType); - - SchemaBuilder WithSize(int size); - - IForeignKeyOptions AsForeignKey(); -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/IDeleteTableOptions.cs b/src/Migrator/Framework/SchemaBuilder/IDeleteTableOptions.cs deleted file mode 100644 index 9b3581fd..00000000 --- a/src/Migrator/Framework/SchemaBuilder/IDeleteTableOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public interface IDeleteTableOptions -{ - SchemaBuilder WithTable(string name); - - SchemaBuilder AddTable(string name); - - IDeleteTableOptions DeleteTable(string name); -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/IFluentColumn.cs b/src/Migrator/Framework/SchemaBuilder/IFluentColumn.cs deleted file mode 100644 index e5a90e0a..00000000 --- a/src/Migrator/Framework/SchemaBuilder/IFluentColumn.cs +++ /dev/null @@ -1,21 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public interface IFluentColumn : IColumn -{ - ForeignKeyConstraintType Constraint { get; set; } - - ForeignKey ForeignKey { get; set; } -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/IForeignKeyOptions.cs b/src/Migrator/Framework/SchemaBuilder/IForeignKeyOptions.cs deleted file mode 100644 index ecd4ecd8..00000000 --- a/src/Migrator/Framework/SchemaBuilder/IForeignKeyOptions.cs +++ /dev/null @@ -1,19 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public interface IForeignKeyOptions -{ - SchemaBuilder ReferencedTo(string primaryKeyTable, string primaryKeyColumn); -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/ISchemaBuilderExpression.cs b/src/Migrator/Framework/SchemaBuilder/ISchemaBuilderExpression.cs deleted file mode 100644 index fe616f96..00000000 --- a/src/Migrator/Framework/SchemaBuilder/ISchemaBuilderExpression.cs +++ /dev/null @@ -1,19 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public interface ISchemaBuilderExpression -{ - void Create(ITransformationProvider provider); -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/RenameTableExpression.cs b/src/Migrator/Framework/SchemaBuilder/RenameTableExpression.cs deleted file mode 100644 index 15625861..00000000 --- a/src/Migrator/Framework/SchemaBuilder/RenameTableExpression.cs +++ /dev/null @@ -1,31 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public class RenameTableExpression : ISchemaBuilderExpression -{ - private readonly string _newName; - private readonly string _oldName; - - public RenameTableExpression(string oldName, string newName) - { - _oldName = oldName; - _newName = newName; - } - - public void Create(ITransformationProvider provider) - { - provider.RenameTable(_oldName, _newName); - } -} \ No newline at end of file diff --git a/src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs b/src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs deleted file mode 100644 index 9aae0607..00000000 --- a/src/Migrator/Framework/SchemaBuilder/SchemaBuilder.cs +++ /dev/null @@ -1,190 +0,0 @@ -#region License - -//The contents of this file are subject to the Mozilla Public License -//Version 1.1 (the "License"); you may not use this file except in -//compliance with the License. You may obtain a copy of the License at -//http://www.mozilla.org/MPL/ -//Software distributed under the License is distributed on an "AS IS" -//basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the -//License for the specific language governing rights and limitations -//under the License. - -#endregion - -using System; -using System.Collections.Generic; -using System.Data; - -namespace DotNetProjects.Migrator.Framework.SchemaBuilder; - -public class SchemaBuilder : IColumnOptions, IForeignKeyOptions, IDeleteTableOptions -{ - private readonly IList _exprs; - private IFluentColumn _currentColumn; - private string _currentTable; - private AddTableExpression _creatingTable; - - public SchemaBuilder() - { - _exprs = new List(); - } - - public IEnumerable Expressions - { - get { return _exprs; } - } - - public SchemaBuilder OfType(DbType columnType) - { - _currentColumn.Type = columnType; - - return this; - } - - public SchemaBuilder WithSize(int size) - { - if (size == 0) - { - throw new ArgumentNullException("size", "Size must be greater than zero"); - } - - _currentColumn.Size = size; - - return this; - } - - public IForeignKeyOptions AsForeignKey() - { - return this; - } - - /// - /// Adds a Table to be created to the Schema - /// - /// Table name to be created - /// SchemaBuilder for chaining - public SchemaBuilder AddTable(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException("name"); - } - - _creatingTable = new AddTableExpression(name); - _exprs.Add(_creatingTable); - _currentTable = name; - - return this; - } - - public IDeleteTableOptions DeleteTable(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException("name"); - } - - _creatingTable = null; - _currentTable = ""; - _currentColumn = null; - - _exprs.Add(new DeleteTableExpression(name)); - - return this; - } - - /// - /// Reference an existing table. - /// - /// Table to reference - /// SchemaBuilder for chaining - public SchemaBuilder WithTable(string name) - { - _creatingTable = null; - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException("name"); - } - - _currentTable = name; - - return this; - } - - public SchemaBuilder ReferencedTo(string primaryKeyTable, string primaryKeyColumn) - { - _currentColumn.Constraint = ForeignKeyConstraintType.NoAction; - _currentColumn.ForeignKey = new ForeignKey(primaryKeyTable, primaryKeyColumn); - return this; - } - - /// - /// Reference an existing table. - /// - /// Table to reference - /// SchemaBuilder for chaining - public SchemaBuilder RenameTable(string newName) - { - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException("newName"); - } - - _creatingTable = null; - _exprs.Add(new RenameTableExpression(_currentTable, newName)); - _currentTable = newName; - - return this; - } - - /// - /// Adds a Column to be created - /// - /// Column name to be added - /// IColumnOptions to restrict chaining - public IColumnOptions AddColumn(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException("name"); - } - - if (string.IsNullOrEmpty(_currentTable)) - { - throw new ArgumentException("missing referenced table"); - } - - IFluentColumn column = new FluentColumn(name); - _currentColumn = column; - - if (_creatingTable != null) _creatingTable.Columns.Add(column); - else _exprs.Add(new AddColumnExpression(_currentTable, column)); - return this; - } - - public SchemaBuilder WithProperty(ColumnProperty columnProperty) - { - _currentColumn.ColumnProperty = columnProperty; - - return this; - } - - public SchemaBuilder WithDefaultValue(object defaultValue) - { - if (defaultValue == null) - { - throw new ArgumentNullException("defaultValue", "DefaultValue cannot be null or empty"); - } - - _currentColumn.DefaultValue = defaultValue; - - return this; - } - - public SchemaBuilder WithConstraint(ForeignKeyConstraintType action) - { - _currentColumn.Constraint = action; - - return this; - } -} \ No newline at end of file diff --git a/src/Migrator/Providers/ColumnPropertiesMapper.cs b/src/Migrator/Providers/ColumnPropertiesMapper.cs index da3e6213..aef0d500 100644 --- a/src/Migrator/Providers/ColumnPropertiesMapper.cs +++ b/src/Migrator/Providers/ColumnPropertiesMapper.cs @@ -1,251 +1,69 @@ +using System; using System.Collections.Generic; using DotNetProjects.Migrator.Framework; namespace DotNetProjects.Migrator.Providers; -/// -/// This is basically a just a helper base class -/// per-database implementors may want to override ColumnSql -/// +/// Renders column attributes. Keys, constraints and indexes belong to the table definition. public class ColumnPropertiesMapper { - /// - /// the type of the column - /// protected string _ColumnSql; - - /// - /// Sql if this column has a default value - /// protected object _DefaultVal; - protected Dialect _Dialect; - - /// - /// Sql if This column is Indexed - /// - protected bool _Indexed; - - /// The name of the column protected string _Name; - - /// The SQL type - public string Type { get; private set; } - - public ColumnPropertiesMapper(Dialect dialect, string typeString) - { - _Dialect = dialect; - Type = typeString; - } - - /// - /// The sql for this column, override in database-specific implementation classes - /// - public virtual string ColumnSql - { - get { return _ColumnSql; } - } - - public string Name - { - get { return _Name; } - set { _Name = value; } - } - - public object Default - { - get { return _DefaultVal; } - set { _DefaultVal = value; } - } - - public string QuotedName - { - get { return _Dialect.Quote(Name); } - } - - public string IndexSql - { - get - { - if (_Dialect.SupportsIndex && _Indexed) - { - return string.Format("INDEX({0})", _Dialect.Quote(_Name)); - } - - return null; - } - } - - public virtual void MapColumnProperties(Column column) + public string Type { get; } + public ColumnPropertiesMapper(Dialect dialect, string typeString) { _Dialect = dialect; Type = typeString; } + public virtual string ColumnSql => _ColumnSql; + public string Name { get => _Name; set => _Name = value; } + public object Default { get => _DefaultVal; set => _DefaultVal = value; } + public string QuotedName => _Dialect.QuoteIdentifier(Name); + public virtual void MapColumnProperties(Column column) => Map(column, true); + public virtual void MapColumnPropertiesWithoutDefault(Column column) => Map(column, false); + private void Map(Column column, bool includeDefault) { Name = column.Name; - - _Indexed = PropertySelected(column.ColumnProperty, ColumnProperty.Indexed); - - var vals = new List(); - - AddName(vals); - - AddType(vals); - - AddCaseSensitive(column, vals); - - AddIdentity(column, vals); - - AddUnsigned(column, vals); - - AddNotNull(column, vals); - - AddNull(column, vals); - - AddPrimaryKey(column, vals); - - AddPrimaryKeyNonClustered(column, vals); - - AddIdentityAgain(column, vals); - - AddUnique(column, vals); - - AddForeignKey(column, vals); - - AddDefaultValue(column, vals); - - _ColumnSql = string.Join(" ", vals.ToArray()); + var values = new List(); + AddName(values); AddType(values); AddCollation(column, values); + AddIdentity(column, values); AddUnsigned(column, values); + AddNotNull(column, values); AddNull(column, values); + AddIdentityAgain(column, values); + if (includeDefault) AddDefaultValue(column, values); + _ColumnSql = string.Join(" ", values); } - - public virtual void MapColumnPropertiesWithoutDefault(Column column) + protected virtual void AddCollation(Column column, List values) { - Name = column.Name; - - _Indexed = PropertySelected(column.ColumnProperty, ColumnProperty.Indexed); - - var vals = new List(); - - AddName(vals); - - AddType(vals); - - AddCaseSensitive(column, vals); - - AddIdentity(column, vals); - - AddUnsigned(column, vals); - - AddNotNull(column, vals); - - AddNull(column, vals); - - AddPrimaryKey(column, vals); - - AddIdentityAgain(column, vals); - - AddPrimaryKeyNonClustered(column, vals); - - AddUnique(column, vals); - - AddForeignKey(column, vals); - - _ColumnSql = string.Join(" ", vals.ToArray()); - } - - protected virtual void AddCaseSensitive(Column column, List vals) - { - AddValueIfSelected(column, ColumnProperty.CaseSensitive, vals); - } - - protected virtual void AddDefaultValue(Column column, List vals) - { - if (column.DefaultValue != null) - { - vals.Add(_Dialect.Default(column.DefaultValue)); - } - } - - protected virtual void AddForeignKey(Column column, List vals) - { - // TODO Does that really make sense? - // AddValueIfSelected(column, ColumnProperty.ForeignKey, vals); + if (!string.IsNullOrWhiteSpace(column.Collation)) + values.Add(_Dialect.GetCollationSql(column.Collation)); } - - protected virtual void AddUnique(Column column, List vals) - { - AddValueIfSelected(column, ColumnProperty.Unique, vals); - } - - protected virtual void AddIdentityAgain(Column column, List vals) - { - if (_Dialect.IdentityNeedsType) - { - AddValueIfSelected(column, ColumnProperty.Identity, vals); - } - } - protected virtual void AddPrimaryKeyNonClustered(Column column, List vals) - { - if (_Dialect.SupportsNonClustered) - { - AddValueIfSelected(column, ColumnProperty.PrimaryKeyNonClustered, vals); - } - } - protected virtual void AddPrimaryKey(Column column, List vals) - { - AddValueIfSelected(column, ColumnProperty.PrimaryKey, vals); - } - - protected virtual void AddNull(Column column, List vals) + protected virtual void AddDefaultValue(Column column, List values) { - if (!PropertySelected(column.ColumnProperty, ColumnProperty.PrimaryKey)) - { - if (_Dialect.NeedsNullForNullableWhenAlteringTable) - { - AddValueIfSelected(column, ColumnProperty.Null, vals); - } - } + if (column.DefaultValue != null) values.Add(_Dialect.Default(column.DefaultValue)); } - - protected virtual void AddNotNull(Column column, List vals) + protected virtual void AddIdentity(Column column, List values) { - if (!PropertySelected(column.ColumnProperty, ColumnProperty.Null) && (!PropertySelected(column.ColumnProperty, ColumnProperty.PrimaryKey) || _Dialect.NeedsNotNullForIdentity)) - { - AddValueIfSelected(column, ColumnProperty.NotNull, vals); - } + if (!_Dialect.IdentityNeedsType && column.IsIdentity) values.Add(_Dialect.SqlForColumnAttribute(ColumnAttribute.Identity, column)); } - - protected virtual void AddUnsigned(Column column, List vals) + protected virtual void AddIdentityAgain(Column column, List values) { - if (_Dialect.IsUnsignedCompatible(column.Type)) - { - AddValueIfSelected(column, ColumnProperty.Unsigned, vals); - } + if (_Dialect.IdentityNeedsType && column.IsIdentity) values.Add(_Dialect.SqlForColumnAttribute(ColumnAttribute.Identity, column)); } - - protected virtual void AddIdentity(Column column, List vals) + protected virtual void AddNull(Column column, List values) { - if (!_Dialect.IdentityNeedsType) - { - AddValueIfSelected(column, ColumnProperty.Identity, vals); - } + if (column.IsNullable && _Dialect.NeedsNullForNullableWhenAlteringTable) + values.Add(_Dialect.SqlForColumnAttribute(ColumnAttribute.Null, column)); } - - protected virtual void AddType(List vals) + protected virtual void AddNotNull(Column column, List values) { - vals.Add(Type); + if (!column.IsNullable) values.Add(_Dialect.SqlForColumnAttribute(ColumnAttribute.NotNull, column)); } - - protected virtual void AddName(List vals) - { - vals.Add(_Dialect.ColumnNameNeedsQuote || _Dialect.IsReservedWord(Name) ? QuotedName : Name); - } - - protected virtual void AddValueIfSelected(Column column, ColumnProperty property, ICollection vals) - { - if (PropertySelected(column.ColumnProperty, property)) - { - vals.Add(_Dialect.SqlForProperty(property, column)); - } - } - - public static bool PropertySelected(ColumnProperty source, ColumnProperty comparison) + protected virtual void AddUnsigned(Column column, List values) { - return (source & comparison) == comparison; + if (!column.IsUnsigned) return; + if (!_Dialect.IsUnsignedCompatible(column.Type)) throw new NotSupportedException("Unsigned is unsupported for this column type."); + var sql = _Dialect.SqlForColumnAttribute(ColumnAttribute.Unsigned, column); + if (string.IsNullOrWhiteSpace(sql)) throw new NotSupportedException("Unsigned columns are unsupported by this dialect."); + values.Add(sql); } + protected virtual void AddType(List values) => values.Add(Type); + protected virtual void AddName(List values) => values.Add(_Dialect.ColumnNameNeedsQuote || _Dialect.IsReservedWord(Name) ? QuotedName : Name); } diff --git a/src/Migrator/Providers/ConstraintMetadataReader.cs b/src/Migrator/Providers/ConstraintMetadataReader.cs index e374c1ea..2d5f8dd5 100644 --- a/src/Migrator/Providers/ConstraintMetadataReader.cs +++ b/src/Migrator/Providers/ConstraintMetadataReader.cs @@ -7,6 +7,8 @@ using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; using DotNetProjects.Migrator.Providers.Impl.Oracle; using DotNetProjects.Migrator.Providers.Impl.Mysql; +using DotNetProjects.Migrator.Providers.Impl.DB2; +using DotNetProjects.Migrator.Providers.Impl.Firebird; using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; namespace DotNetProjects.Migrator.Providers; @@ -34,6 +36,26 @@ FROM pg_constraint c LEFT JOIN LATERAL unnest(c.conkey) WITH ORDINALITY k(attnum LEFT JOIN pg_attribute a ON a.attrelid=c.conrelid AND a.attnum=k.attnum WHERE c.conrelid=to_regclass(@lookup_table) AND c.contype IN ('p','u','c') ORDER BY c.conname,k.ordinality"; } + else if (provider.Dialect is DB2Dialect) + { + parameterTable = table.StartsWith('"') ? table.Trim('"').Replace("\"\"", "\"") : table.ToUpperInvariant(); + sql = @"SELECT c.CONSTNAME,c.TYPE,k.COLNAME,k.COLSEQ,ch.TEXT + FROM SYSCAT.TABCONST c LEFT JOIN SYSCAT.KEYCOLUSE k + ON k.TABSCHEMA=c.TABSCHEMA AND k.TABNAME=c.TABNAME AND k.CONSTNAME=c.CONSTNAME AND c.TYPE IN ('P','U') + LEFT JOIN SYSCAT.CHECKS ch ON ch.TABSCHEMA=c.TABSCHEMA AND ch.TABNAME=c.TABNAME AND ch.CONSTNAME=c.CONSTNAME + WHERE c.TABSCHEMA=CURRENT SCHEMA AND c.TABNAME=@lookup_table AND c.TYPE IN ('P','U','K') + ORDER BY c.CONSTNAME,k.COLSEQ"; + } + else if (provider.Dialect is FirebirdDialect) + { + parameterTable = table.StartsWith('"') ? table.Trim('"').Replace("\"\"", "\"") : table.ToUpperInvariant(); + sql = @"SELECT TRIM(c.RDB$CONSTRAINT_NAME),TRIM(c.RDB$CONSTRAINT_TYPE),TRIM(k.RDB$FIELD_NAME),k.RDB$FIELD_POSITION, + (SELECT FIRST 1 t.RDB$TRIGGER_SOURCE FROM RDB$CHECK_CONSTRAINTS ch JOIN RDB$TRIGGERS t ON t.RDB$TRIGGER_NAME=ch.RDB$TRIGGER_NAME + WHERE ch.RDB$CONSTRAINT_NAME=c.RDB$CONSTRAINT_NAME) + FROM RDB$RELATION_CONSTRAINTS c LEFT JOIN RDB$INDEX_SEGMENTS k ON k.RDB$INDEX_NAME=c.RDB$INDEX_NAME AND c.RDB$CONSTRAINT_TYPE IN ('PRIMARY KEY','UNIQUE') + WHERE c.RDB$RELATION_NAME=@lookup_table AND c.RDB$CONSTRAINT_TYPE IN ('PRIMARY KEY','UNIQUE','CHECK') + ORDER BY c.RDB$CONSTRAINT_NAME,k.RDB$FIELD_POSITION"; + } else if (oracle || provider.Dialect is MysqlDialect) { // Quoted identifiers containing a dot need a structured name API rather than ambiguous splitting. @@ -80,7 +102,7 @@ void Complete() { "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)), + "C" or "K" or "CHECK" => new CheckConstraint(name, reader.IsDBNull(4) ? null : CheckExpression(reader.GetString(4))), _ => throw new MigrationException("Unknown catalog constraint type.") }; } @@ -92,6 +114,17 @@ void Complete() return constraints.ToArray(); } + internal static string CheckExpression(string source) + { + var text = source.Trim(); + if (text.StartsWith("CHECK", StringComparison.OrdinalIgnoreCase)) + { + text = text[5..].Trim(); + if (text.StartsWith("(") && text.EndsWith(")")) text = text[1..^1]; + } + return text; + } + private static void AddParameter(IDbCommand command, string name, object value) { var parameter = command.CreateParameter(); parameter.ParameterName = name; diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index 06728b1f..91ec754f 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -15,7 +15,7 @@ namespace DotNetProjects.Migrator.Providers; /// public abstract class Dialect : IDialect { - private readonly Dictionary _propertyMap = []; + private readonly Dictionary _propertyMap = []; private readonly HashSet _reservedWords = []; private readonly TypeNames _typeNames = new(); private readonly List _unsignedCompatibleTypes = []; @@ -31,17 +31,16 @@ public abstract class Dialect : IDialect protected Dialect() { - RegisterProperty(ColumnProperty.Null, "NULL"); - RegisterProperty(ColumnProperty.NotNull, "NOT NULL"); - RegisterProperty(ColumnProperty.Unique, "UNIQUE"); - RegisterProperty(ColumnProperty.PrimaryKey, "PRIMARY KEY"); - RegisterProperty(ColumnProperty.PrimaryKeyNonClustered, " NONCLUSTERED"); + RegisterColumnAttribute(ColumnAttribute.Null, "NULL"); + RegisterColumnAttribute(ColumnAttribute.NotNull, "NOT NULL"); } /// Render a named table constraint without accessing a database. + public virtual string GetCollationSql(string name) => throw new NotSupportedException("Column collations are not supported by this dialect."); + public virtual string GetTableConstraintSql(TableConstraint constraint) { - if (string.IsNullOrWhiteSpace(constraint.Name)) throw new MigrationException("A constraint name is required."); + if (constraint.Name != null && string.IsNullOrWhiteSpace(constraint.Name)) throw new MigrationException("A constraint name must not be empty."); string Keys(string[] columns) => string.Join(", ", columns.Select(name => ColumnNameNeedsQuote || IsReservedWord(name) ? QuoteIdentifier(name) : name)); var body = constraint switch { @@ -51,7 +50,7 @@ public virtual string GetTableConstraintSql(TableConstraint constraint) 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}"; + return constraint.Name == null ? body : $"CONSTRAINT {QuoteIdentifier(constraint.Name)} {body}"; } /// Quote one identifier atom, escaping its delimiter; never split a name on dots. @@ -351,7 +350,7 @@ public virtual DbType GetDbType(string databaseTypeName) return _typeNames.GetDbType(databaseTypeName); } - public void RegisterProperty(ColumnProperty property, string sql) + public void RegisterColumnAttribute(ColumnAttribute property, string sql) { if (!_propertyMap.ContainsKey(property)) { @@ -360,7 +359,7 @@ public void RegisterProperty(ColumnProperty property, string sql) _propertyMap[property] = sql; } - public virtual string SqlForProperty(ColumnProperty property, Column column) + public virtual string SqlForColumnAttribute(ColumnAttribute property, Column column) { if (_propertyMap.ContainsKey(property)) { diff --git a/src/Migrator/Providers/Impl/DB2/DB2Dialect.cs b/src/Migrator/Providers/Impl/DB2/DB2Dialect.cs index aeb19070..f84d6fa7 100644 --- a/src/Migrator/Providers/Impl/DB2/DB2Dialect.cs +++ b/src/Migrator/Providers/Impl/DB2/DB2Dialect.cs @@ -39,7 +39,7 @@ public DB2Dialect() RegisterColumnType(DbType.AnsiString, int.MaxValue, "CLOB"); RegisterColumnType(DbType.StringFixedLength, 255, "CHAR($l)"); RegisterColumnType(DbType.AnsiStringFixedLength, 255, "CHAR($l)"); - RegisterProperty(ColumnProperty.Identity, "GENERATED BY DEFAULT AS IDENTITY"); + RegisterColumnAttribute(ColumnAttribute.Identity, "GENERATED BY DEFAULT AS IDENTITY"); } public override ColumnPropertiesMapper GetColumnMapper(Column column) @@ -55,16 +55,17 @@ private sealed class NativeColumnMapper(Dialect dialect, string type) : ColumnPr public override void MapColumnProperties(Column column) { Name = column.Name; - _Indexed = PropertySelected(column.ColumnProperty, ColumnProperty.Indexed); + var parts = new System.Collections.Generic.List(); AddName(parts); AddType(parts); + AddCollation(column, parts); + AddUnsigned(column, parts); AddIdentityAgain(column, parts); AddDefaultValue(column, parts); - if (column.IsPrimaryKey || column.ColumnProperty.HasFlag(ColumnProperty.NotNull)) + if (!column.IsNullable) parts.Add("NOT NULL"); - AddPrimaryKey(column, parts); - AddUnique(column, parts); + _ColumnSql = string.Join(" ", parts); } } diff --git a/src/Migrator/Providers/Impl/DB2/DB2TransformationProvider.cs b/src/Migrator/Providers/Impl/DB2/DB2TransformationProvider.cs index 11fed953..d36a01db 100644 --- a/src/Migrator/Providers/Impl/DB2/DB2TransformationProvider.cs +++ b/src/Migrator/Providers/Impl/DB2/DB2TransformationProvider.cs @@ -30,8 +30,6 @@ public override void AddColumn(string table, Column column) => public override void AddTable(string name, string engine, params IDbField[] fields) { base.AddTable(name, engine, fields); - foreach (var column in fields.OfType().Where(c => c.ColumnProperty.HasFlag(ColumnProperty.Indexed))) - AddIndex(name, new Index { KeyColumns = [column.Name] }); } public override bool TableExists(string table) => Convert.ToInt32(ExecuteScalar( @@ -67,7 +65,7 @@ public override Column[] GetColumns(string table) }; var column = new Column(reader.GetString(0).Trim(), type) { - ColumnProperty = reader.GetString(2) == "Y" ? ColumnProperty.Null : ColumnProperty.NotNull + IsNullable = reader.GetString(2) == "Y" }; if (!reader.IsDBNull(3)) column.DefaultValue = CatalogDefaultValue.Parse(reader.GetString(3), type); if (type == DbType.String) column.Size = Convert.ToInt32(reader.GetValue(4)); @@ -77,8 +75,7 @@ public override Column[] GetColumns(string table) column.Scale = Convert.ToInt32(reader.GetValue(7)); } if (type == DbType.VarNumeric) column.Precision = Convert.ToInt32(reader.GetValue(4)) == 8 ? 16 : 34; - if (reader.GetString(5) == "Y") column.ColumnProperty |= ColumnProperty.Identity; - if (!reader.IsDBNull(6)) column.ColumnProperty |= ColumnProperty.PrimaryKey; + if (reader.GetString(5) == "Y") column.IsIdentity = true; columns.Add(column); } return columns.ToArray(); @@ -129,17 +126,14 @@ public override string AddIndex(string table, Index index) public override void ChangeColumn(string table, Column column) { - var isUniqueSet = column.ColumnProperty.HasFlag(ColumnProperty.Unique); - column.ColumnProperty &= ~ColumnProperty.Unique; + var prefix = $"ALTER TABLE {Identifier(table)} ALTER COLUMN {Identifier(column.Name)}"; var type = _dialect.GetColumnMapper(column).Type; ExecuteNonQuery($"{prefix} SET DATA TYPE {type}"); if (column.DefaultValue != null || GetColumns(table).Single(c => c.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)).DefaultValue != null) ExecuteNonQuery($"{prefix} {(column.DefaultValue == null ? "DROP DEFAULT" : "SET " + _dialect.Default(column.DefaultValue))}"); - ExecuteNonQuery($"{prefix} {(column.ColumnProperty.HasFlag(ColumnProperty.NotNull) ? "SET" : "DROP")} NOT NULL"); + ExecuteNonQuery($"{prefix} {(!column.IsNullable ? "SET" : "DROP")} NOT NULL"); Reorganize(table); - if (isUniqueSet) - AddUniqueConstraint($"UX_{table}_{column.Name}", table, [column.Name]); } public override void RemoveColumn(string tableName, string column) diff --git a/src/Migrator/Providers/Impl/Firebird/FirebirdColumnPropertiesMapper.cs b/src/Migrator/Providers/Impl/Firebird/FirebirdColumnPropertiesMapper.cs index a9a4d833..afee889b 100644 --- a/src/Migrator/Providers/Impl/Firebird/FirebirdColumnPropertiesMapper.cs +++ b/src/Migrator/Providers/Impl/Firebird/FirebirdColumnPropertiesMapper.cs @@ -14,23 +14,19 @@ public override void MapColumnProperties(Column column) { Name = column.Name; - _Indexed = PropertySelected(column.ColumnProperty, ColumnProperty.Indexed); var vals = new List(); AddName(vals); AddType(vals); + AddCollation(column, vals); + AddUnsigned(column, vals); AddIdentity(column, vals); AddIdentityAgain(column, vals); - AddPrimaryKey(column, vals); - - AddUnique(column, vals); - - AddForeignKey(column, vals); AddDefaultValue(column, vals); diff --git a/src/Migrator/Providers/Impl/Firebird/FirebirdDialect.cs b/src/Migrator/Providers/Impl/Firebird/FirebirdDialect.cs index 3d9f85d8..75061921 100644 --- a/src/Migrator/Providers/Impl/Firebird/FirebirdDialect.cs +++ b/src/Migrator/Providers/Impl/Firebird/FirebirdDialect.cs @@ -5,6 +5,8 @@ namespace DotNetProjects.Migrator.Providers.Impl.Firebird; public class FirebirdDialect : Dialect { + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); + // This flag controls MySQL-style inline INDEX syntax, not CREATE INDEX support. public override bool SupportsIndex => false; @@ -34,7 +36,7 @@ public FirebirdDialect() RegisterColumnType(DbType.String, int.MaxValue, "BLOB SUB_TYPE TEXT"); RegisterColumnType(DbType.Time, "TIME"); - RegisterProperty(ColumnProperty.Identity, "GENERATED BY DEFAULT AS IDENTITY"); + RegisterColumnAttribute(ColumnAttribute.Identity, "GENERATED BY DEFAULT AS IDENTITY"); this.RegisterUnsignedCompatible(DbType.Int16); this.RegisterUnsignedCompatible(DbType.Int32); diff --git a/src/Migrator/Providers/Impl/Firebird/FirebirdTransformationProvider.cs b/src/Migrator/Providers/Impl/Firebird/FirebirdTransformationProvider.cs index 062643c8..dab77056 100644 --- a/src/Migrator/Providers/Impl/Firebird/FirebirdTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Firebird/FirebirdTransformationProvider.cs @@ -32,8 +32,6 @@ public override void AddColumn(string table, Column column) => public override void AddTable(string name, string engine, params IDbField[] fields) { base.AddTable(name, engine, fields); - foreach (var column in fields.OfType().Where(c => c.ColumnProperty.HasFlag(ColumnProperty.Indexed))) - AddIndex(name, new Index { KeyColumns = [column.Name] }); } public override bool TableExists(string table) => Convert.ToInt32(ExecuteScalar( @@ -98,7 +96,7 @@ SELECT TRIM(r.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, r.RDB$NULL_FLAG, type = DbType.Decimal; var column = new Column(reader.GetString(0), type) { - ColumnProperty = !reader.IsDBNull(2) && Convert.ToInt32(reader.GetValue(2)) == 1 ? ColumnProperty.NotNull : ColumnProperty.Null + IsNullable = !(!reader.IsDBNull(2) && Convert.ToInt32(reader.GetValue(2)) == 1) }; if (type == DbType.Decimal) { @@ -108,8 +106,7 @@ SELECT TRIM(r.RDB$FIELD_NAME), f.RDB$FIELD_TYPE, r.RDB$NULL_FLAG, if (!reader.IsDBNull(3)) column.DefaultValue = ReadDefault(reader.GetString(3), type); if (!reader.IsDBNull(4)) column.Size = Convert.ToInt32(reader.GetValue(4)); if (Convert.ToInt32(reader.GetValue(1)) == 261 && type == DbType.String) column.Size = int.MaxValue; - if (!reader.IsDBNull(5)) column.ColumnProperty |= ColumnProperty.Identity; - if (primaryColumns.Contains(column.Name)) column.ColumnProperty |= ColumnProperty.PrimaryKey; + if (!reader.IsDBNull(5)) column.IsIdentity = true; result.Add(column); } return result.ToArray(); @@ -160,16 +157,13 @@ public override void RenameColumn(string tableName, string oldColumnName, string public override void ChangeColumn(string table, Column column) { - var isUniqueSet = column.ColumnProperty.HasFlag(ColumnProperty.Unique); - column.ColumnProperty &= ~ColumnProperty.Unique; + var prefix = $"ALTER TABLE {QuoteTableNameIfRequired(table)} ALTER {QuoteColumnNameIfRequired(column.Name)}"; var type = _dialect.GetColumnMapper(column).Type; ExecuteNonQuery($"{prefix} TYPE {type}"); if (column.DefaultValue != null || GetColumns(table).Single(c => c.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)).DefaultValue != null) ExecuteNonQuery($"{prefix} {(column.DefaultValue == null ? "DROP DEFAULT" : "SET " + _dialect.Default(column.DefaultValue))}"); - ExecuteNonQuery($"{prefix} {(column.ColumnProperty.HasFlag(ColumnProperty.NotNull) ? "SET" : "DROP")} NOT NULL"); - if (isUniqueSet) - AddUniqueConstraint($"UX_{table}_{column.Name}", table, [column.Name]); + ExecuteNonQuery($"{prefix} {(!column.IsNullable ? "SET" : "DROP")} NOT NULL"); } public override string AddIndex(string table, Index index) diff --git a/src/Migrator/Providers/Impl/Informix/InformixDialect.cs b/src/Migrator/Providers/Impl/Informix/InformixDialect.cs index 18f376d3..2e3befc7 100644 --- a/src/Migrator/Providers/Impl/Informix/InformixDialect.cs +++ b/src/Migrator/Providers/Impl/Informix/InformixDialect.cs @@ -39,7 +39,7 @@ public InformixDialect() RegisterColumnType(DbType.AnsiStringFixedLength, 32767, "CHAR($l)"); RegisterColumnType(DbType.String, int.MaxValue, "TEXT"); RegisterColumnType(DbType.AnsiString, int.MaxValue, "TEXT"); - RegisterProperty(ColumnProperty.Identity, ""); + RegisterColumnAttribute(ColumnAttribute.Identity, ""); } public override string Default(object value) => value is bool boolean ? (boolean ? "DEFAULT 't'" : "DEFAULT 'f'") : base.Default(value); @@ -58,15 +58,16 @@ private sealed class NativeColumnMapper(Dialect dialect, string type) : ColumnPr public override void MapColumnProperties(Column column) { Name = column.Name; - _Indexed = PropertySelected(column.ColumnProperty, ColumnProperty.Indexed); + var parts = new System.Collections.Generic.List(); AddName(parts); AddType(parts); + AddCollation(column, parts); + AddUnsigned(column, parts); AddIdentityAgain(column, parts); AddDefaultValue(column, parts); AddNotNull(column, parts); - AddPrimaryKey(column, parts); - AddUnique(column, parts); + _ColumnSql = string.Join(" ", parts); } } diff --git a/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs b/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs index 5b3a7c90..199da853 100644 --- a/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs @@ -29,8 +29,6 @@ public override void AddColumn(string table, Column column) => public override void AddTable(string name, string engine, params IDbField[] fields) { base.AddTable(name, engine, fields); - foreach (var column in fields.OfType().Where(c => c.ColumnProperty.HasFlag(ColumnProperty.Indexed))) - AddIndex(name, new Index { KeyColumns = [column.Name] }); } public override bool TableExists(string table) => Convert.ToInt32(ExecuteScalar( @@ -74,7 +72,7 @@ public override Column[] GetColumns(string table) if (extendedType == "boolean") type = DbType.Boolean; var column = new Column(reader.GetString(0).Trim(), type) { - ColumnProperty = (code & 256) != 0 ? ColumnProperty.NotNull : ColumnProperty.Null + IsNullable = !((code & 256) != 0) }; if (type is DbType.String or DbType.StringFixedLength) { @@ -90,14 +88,43 @@ public override Column[] GetColumns(string table) column.Precision = length >> 8; column.Scale = (length & 255) == 255 ? null : length & 255; } - if ((code & 255) is 6 or 18 or 53) column.ColumnProperty |= ColumnProperty.Identity; + if ((code & 255) is 6 or 18 or 53) column.IsIdentity = true; if (!reader.IsDBNull(5)) column.DefaultValue = ReadDefault(reader.IsDBNull(3) ? "" : reader.GetString(3), reader.GetString(5).Trim(), type); - if (primaryColumns.Contains(column.Name)) column.ColumnProperty |= ColumnProperty.PrimaryKey; columns.Add(column); } return columns.ToArray(); } + public override TableConstraint[] GetTableConstraints(string table) + { + var indexes = GetIndexes(table).ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); + var constraints = new List(); + using (var command = CreateCommand()) + using (var reader = ExecuteQuery(command, $"SELECT c.constrname,c.constrtype,c.idxname FROM sysconstraints c JOIN systables t ON t.tabid=c.tabid WHERE t.owner=USER AND t.tabname='{Name(table)}' AND c.constrtype IN ('P','U') ORDER BY c.constrname")) + { + while (reader.Read()) + { + var name = reader.GetString(0).Trim(); + var index = indexes[reader.GetString(2).Trim()]; + constraints.Add(reader.GetString(1).Trim() == "P" + ? new PrimaryKeyConstraint(name, index.KeyColumns) + : new DotNetProjects.Migrator.Framework.UniqueConstraint(name, index.KeyColumns)); + } + } + var checks = new Dictionary(); + using (var command = CreateCommand()) + using (var reader = ExecuteQuery(command, $"SELECT c.constrname,ch.checktext FROM sysconstraints c JOIN systables t ON t.tabid=c.tabid JOIN syschecks ch ON ch.constrid=c.constrid WHERE t.owner=USER AND t.tabname='{Name(table)}' AND c.constrtype='C' AND ch.type='T' ORDER BY c.constrname,ch.seqno")) + while (reader.Read()) + { + var name = reader.GetString(0).Trim(); + if (!checks.TryGetValue(name, out var text)) checks[name] = text = new System.Text.StringBuilder(); + text.Append(reader.GetString(1)); + } + constraints.AddRange(checks.Select(c => new CheckConstraint(c.Key, ConstraintMetadataReader.CheckExpression(c.Value.ToString())))); + constraints.AddRange(GetForeignKeyConstraints(table)); + return constraints.ToArray(); + } + private static object ReadDefault(string catalogValue, string kind, DbType type) { // SYSDEFAULTS stores literal text without SQL quotes, and prefixes non-character diff --git a/src/Migrator/Providers/Impl/Ingres/IngresDialect.cs b/src/Migrator/Providers/Impl/Ingres/IngresDialect.cs index a6ce8103..faf61259 100644 --- a/src/Migrator/Providers/Impl/Ingres/IngresDialect.cs +++ b/src/Migrator/Providers/Impl/Ingres/IngresDialect.cs @@ -46,8 +46,8 @@ public IngresDialect() this.RegisterColumnType(DbType.String, 1073741823, "LONGTEXT"); this.RegisterColumnType(DbType.Time, "TIME"); - this.RegisterProperty(ColumnProperty.Unsigned, "UNSIGNED"); - this.RegisterProperty(ColumnProperty.Identity, "AUTO_INCREMENT"); + this.RegisterColumnAttribute(ColumnAttribute.Unsigned, "UNSIGNED"); + this.RegisterColumnAttribute(ColumnAttribute.Identity, "AUTO_INCREMENT"); this.RegisterUnsignedCompatible(DbType.Int16); this.RegisterUnsignedCompatible(DbType.Int32); diff --git a/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs b/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs index 5ad933d7..7e06754c 100644 --- a/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Mysql/MySqlTransformationProvider.cs @@ -207,9 +207,8 @@ public override Column[] GetColumns(string table) "tinyblob" or "mediumblob" or "blob" or "binary" or "varbinary" or "longblob" => DbType.Binary, _ => DbType.String }; var column = new Column(reader.GetString(0), type); - column.ColumnProperty = reader.GetString(2) == "YES" ? ColumnProperty.Null : ColumnProperty.NotNull; - if (reader.GetString(4).Contains("auto_increment")) column.ColumnProperty |= ColumnProperty.Identity; - if (reader.GetString(6) == "PRI") column.ColumnProperty |= ColumnProperty.PrimaryKey; + column.IsNullable = reader.GetString(2) == "YES"; + if (reader.GetString(4).Contains("auto_increment")) column.IsIdentity = true; if (!reader.IsDBNull(3)) column.DefaultValue = ReadDefault(reader.GetString(3), type, reader.GetString(4)); if (type == DbType.Decimal) { diff --git a/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs b/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs index fa51e466..9874aeaa 100644 --- a/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs +++ b/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs @@ -5,6 +5,8 @@ namespace DotNetProjects.Migrator.Providers.Impl.Mysql; public class MysqlDialect : Dialect { + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); + public MysqlDialect() { // TODO: As per http://dev.mysql.com/doc/refman/5.0/en/char.html 5.0.3 and above @@ -52,9 +54,8 @@ public MysqlDialect() RegisterColumnType(DbType.String, int.MaxValue, "LONGTEXT"); RegisterColumnType(DbType.Time, "TIME"); - RegisterProperty(ColumnProperty.Unsigned, "UNSIGNED"); - RegisterProperty(ColumnProperty.Identity, "AUTO_INCREMENT"); - RegisterProperty(ColumnProperty.CaseSensitive, "BINARY"); + RegisterColumnAttribute(ColumnAttribute.Unsigned, "UNSIGNED"); + RegisterColumnAttribute(ColumnAttribute.Identity, "AUTO_INCREMENT"); RegisterUnsignedCompatible(DbType.Int16); RegisterUnsignedCompatible(DbType.Int32); diff --git a/src/Migrator/Providers/Impl/Oracle/OracleColumnPropertiesMapper.cs b/src/Migrator/Providers/Impl/Oracle/OracleColumnPropertiesMapper.cs index 56f672d4..b3e0a9d7 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleColumnPropertiesMapper.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleColumnPropertiesMapper.cs @@ -13,25 +13,21 @@ public override void MapColumnProperties(Column column) { Name = column.Name; - _Indexed = PropertySelected(column.ColumnProperty, ColumnProperty.Indexed); var vals = new List(); AddName(vals); AddType(vals); + AddCollation(column, vals); AddIdentity(column, vals); AddUnsigned(column, vals); - AddPrimaryKey(column, vals); AddIdentityAgain(column, vals); - AddUnique(column, vals); - - AddForeignKey(column, vals); AddDefaultValue(column, vals); diff --git a/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs b/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs index 4d5a2b32..49fef510 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs @@ -6,6 +6,8 @@ namespace DotNetProjects.Migrator.Providers.Impl.Oracle; public class OracleDialect : Dialect { + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); + public OracleDialect() { RegisterColumnType(DbType.AnsiStringFixedLength, "CHAR(255)"); @@ -50,13 +52,13 @@ public OracleDialect() RegisterColumnType(DbType.Guid, "RAW(16)"); RegisterColumnType(MigratorDbType.Interval, "interval day (9) to second (9)"); - RegisterProperty(ColumnProperty.Identity, "GENERATED ALWAYS AS IDENTITY"); + RegisterColumnAttribute(ColumnAttribute.Identity, "GENERATED ALWAYS AS IDENTITY"); // the original Migrator.Net code had this, but it's a bad idea - when // apply a "null" migration to a "not-null" field, it just leaves it as "not-null" and it silently fails // because Oracle doesn't consider ALTER TABLE
MODIFY (column ) as being a request to make the field null. - //RegisterProperty(ColumnProperty.Null, String.Empty); + //RegisterColumnAttribute(ColumnAttribute.Null, String.Empty); AddReservedWords("ACCOUNT", "ACTIVATE", "ADMIN", "ADVISE", "AFTER", "ALL_ROWS", "ALLOCATE", "ANALYZE", "ARCHIVE", "ARCHIVELOG", "ARRAY", "AT", "AUTHENTICATED", "AUTHORIZATION", "AUTOEXTEND", "AUTOMATIC", "BACKUP", "BECOME", "BEFORE", "BEGIN", "BFILE", "BITMAP", "BLOB", "BLOCK", "BODY", "CACHE", "CACHE_INSTANCES", "CANCEL", "CASCADE", "CAST", "CFILE", "CHAINED", "CHANGE", "CHAR_CS", "CHARACTER", "CHECKPOINT", "CHOOSE", "CHUNK", "CLEAR", "CLOB", "CLONE", "CLOSE", "CLOSE_CACHED_OPEN_CURSORS", "COALESCE", "COLUMNS", "COMMIT", "COMMITTED", "COMPATIBILITY", "COMPILE", "COMPLETE", "COMPOSITE_LIMIT", "COMMENT", "COMPUTE", "CONNECT_TIME", "CONSTRAINT", "CONSTRAINTS", "CONTENTS", "CONTINUE", "CONTROLFILE", "CONVERT", "COST", "CPU_PER_CALL", "CPU_PER_SESSION", "CURRENT_SCHEMA", "CURREN_USER", "CURSOR", "CYCLE", "DANGLING", "DATABASE", "DATAFILE", "DATAFILES", "DATAOBJNO", "DBA", "DBHIGH", "DBLOW", "DBMAC", "DEALLOCATE", "DEBUG", "DEC", "DECLARE", "DEFERRABLE", "DEFERRED", "DEGREE", "DEREF", "DIRECTORY", "DISABLE", "DISCONNECT", "DISMOUNT", "DISTRIBUTED", "DML", "DOUBLE", "DUMP", "EACH", "ENABLE", "END", "ENFORCE", "ENTRY", "ESCAPE", "EXCEPT", "EXCEPTIONS", "EXCHANGE", "EXCLUDING", "EXECUTE", "EXPIRE", "EXPLAIN", "EXTENT", "EXTENTS", "EXTERNALLY", "FAILED_LOGIN_ATTEMPTS", "FALSE", "FAST", "FIRST_ROWS", "FLAGGER", "FLOB", "FLUSH", "FORCE", "FOREIGN", "FREELIST", "FREELISTS", "FULL", "FUNCTION", "GLOBAL", "GLOBALLY", "GLOBAL_NAME", "GROUPS", "HASH", "HASHKEYS", "HEADER", "HEAP", "IDGENERATORS", "IDLE_TIME", "IF", "INCLUDING", "INCREMENT", "INDEXED", "INDEXES", "INDICATOR", "IND_PARTITION", "INITIALLY", "INITRANS", "INSTANCE", "INSTANCES", "INSTEAD", "INT", "INTERMEDIATE", "ISOLATION", "ISOLATION_LEVEL", "KEEP", "KEY", "KILL", "LABEL", "LAYER", "LESS", "LIBRARY", "LIMIT", "LINK", "LIST", "LOB", "LOCAL", "LOCKED", "LOG", "LOGFILE", "LOGGING", "LOGICAL_READS_PER_CALL", "LOGICAL_READS_PER_SESSION", "MANAGE", "MASTER", "MAX", "MAXARCHLOGS", "MAXDATAFILES", "MAXINSTANCES", "MAXLOGFILES", "MAXLOGHISTORY", "MAXLOGMEMBERS", "MAXSIZE", "MAXTRANS", "MAXVALUE", "MIN", "MEMBER", "MINIMUM", "MINEXTENTS", "MINVALUE", "MLS_LABEL_FORMAT", "MOUNT", "MOVE", "MTS_DISPATCHERS", "MULTISET", "NATIONAL", "NCHAR", "NCHAR_CS", "NCLOB", "NEEDED", "NESTED", "NETWORK", "NEW", "NEXT", "NOARCHIVELOG", "NOCACHE", "NOCYCLE", "NOFORCE", "NOLOGGING", "NOMAXVALUE", "NOMINVALUE", "NONE", "NOORDER", "NOOVERRIDE", "NOPARALLEL", "NOPARALLEL", "NOREVERSE", "NORMAL", "NOSORT", "NOTHING", "NUMBER", "NUMERIC", "NVARCHAR2", "OBJECT", "OBJNO", "OBJNO_REUSE", "OFF", "OID", "OIDINDEX", "OLD", "ONLY", "OPCODE", "OPEN", "OPTIMAL", "OPTIMIZER_GOAL", "ORGANIZATION", "OSLABEL", "OVERFLOW", "OWN", "ORDER", "PACKAGE", "PARALLEL", "PARTITION", "PASSWORD", "PASSWORD_GRACE_TIME", "PASSWORD_LIFE_TIME", "PASSWORD_LOCK_TIME", "PASSWORD_REUSE_MAX", "PASSWORD_REUSE_TIME", "PASSWORD_VERIFY_FUNCTION", "PCTINCREASE", "PCTTHRESHOLD", "PCTUSED", "PCTVERSION", "PERCENT", "PERMANENT", "PLAN", "PLSQL_DEBUG", "POST_TRANSACTION", "PRECISION", "PRESERVE", "PRIMARY", "PRIVATE", "PRIVATE_SGA", "PRIVILEGE", "PROCEDURE", "PROFILE", "PURGE", "QUEUE", "QUOTA", "RANGE", "RBA", "READ", "READUP", "REAL", "REBUILD", "RECOVER", "RECOVERABLE", "RECOVERY", "REF", "REFERENCES", "REFERENCING", "REFRESH", "REPLACE", "RESET", "RESETLOGS", "RESIZE", "RESTRICTED", "RETURN", "RETURNING", "REUSE", "REVERSE", "ROLE", "ROLES", "ROLLBACK", "RULE", "SAMPLE", "SAVEPOINT", "SB4", "SCAN_INSTANCES", "SCHEMA", "SCN", "SCOPE", "SD_ALL", "SD_INHIBIT", "SD_SHOW", "SEGMENT", "SEG_BLOCK", "SEG_FILE", "SEQUENCE", "SERIALIZABLE", "SESSION_CACHED_CURSORS", "SESSIONS_PER_USER", "SIZE", "SHARED", "SHARED_POOL", "SHRINK", "SKIP", "SKIP_UNUSABLE_INDEXES", "SNAPSHOT", "SOME", "SORT", "SPECIFICATION", "SPLIT", "SQL_TRACE", "STANDBY", "STATEMENT_ID", "STATISTICS", "STOP", "STORAGE", "STORE", "STRUCTURE", "SWITCH", "SYS_OP_ENFORCE_NOT_NULL$", "SYS_OP_NTCIMG$", "SYSDBA", "SYSOPER", "SYSTEM", "TABLES", "TABLESPACE", "TABLESPACE_NO", "TABNO", "TEMPORARY", "THAN", "THE", "THREAD", "TIMESTAMP", "TIME", "TOPLEVEL", "TRACE", "TRACING", "TRANSACTION", "TRANSITIONAL", "TRIGGERS", "TRUE", "TRUNCATE", "TX", "TYPE", "UB2", "UBA", "UNARCHIVED", "UNDO", "UNLIMITED", "UNLOCK", "UNRECOVERABLE", "UNTIL", "UNUSABLE", "UNUSED", "UPDATABLE", "USAGE", "USE", "USING", "VALIDATION", "VALUE", "VALUES", "VARYING", "VIEW", "WHEN", "WITHOUT", "WORK", "WRITE", "WRITEDOWN", "WRITEUP", "XID", "YEAR", "ZONE"); } diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index 1b3a7ddd..2a8ae3b9 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -196,56 +196,18 @@ protected override string GetPrimaryKeyname(string tableName) public override void ChangeColumn(string table, Column column) { - column = column.CopyDefinition(); - var existingColumn = GetColumnByName(table, column.Name); - - if (column.Type == DbType.String) - { - RenameColumn(table, column.Name, TemporaryColumnName); - - // check if this is not-null - var isNotNull = (column.ColumnProperty & ColumnProperty.NotNull) == ColumnProperty.NotNull; - - // remove the not-null option - column.ColumnProperty = (column.ColumnProperty & ~ColumnProperty.NotNull); - - AddColumn(table, column); - CopyDataFromOneColumnToAnother(table, TemporaryColumnName, column.Name); - RemoveColumn(table, TemporaryColumnName); - //RenameColumn(table, TemporaryColumnName, column.Name); - - var columnName = QuoteColumnNameIfRequired(column.Name); - - // now set the column to not-null - if (isNotNull) - { - using var cmd = CreateCommand(); - ExecuteQuery(cmd, string.Format("ALTER TABLE {0} MODIFY ({1} NOT NULL)", table, columnName)); - } - } - else - { - // String changes replace the column, which already removes its default. - // For in-place changes Oracle otherwise retains the existing default. - if (column.DefaultValue == null) RemoveColumnDefaultValue(table, column.Name); - if (((existingColumn.ColumnProperty & ColumnProperty.NotNull) == ColumnProperty.NotNull) - && ((column.ColumnProperty & ColumnProperty.NotNull) == ColumnProperty.NotNull)) - { - // was not null, and is being change to not-null - drop the not-null all together - column.ColumnProperty = column.ColumnProperty & ~ColumnProperty.NotNull; - } - else if - (((existingColumn.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null) - && ((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null)) - { - // was null, and is being changed to null - drop the null all together - column.ColumnProperty = column.ColumnProperty & ~ColumnProperty.Null; - } - - var mapper = _dialect.GetAndMapColumnProperties(column); - - ChangeColumn(table, mapper.ColumnSql); - } + var existing = GetColumnByName(table, column.Name); + var definition = column.CopyDefinition(); + if (definition.DefaultValue == null) RemoveColumnDefaultValue(table, definition.Name); + // Oracle rejects restating an existing NOT NULL constraint. Render type/default + // separately and change nullability only when its value actually changes. + definition.IsNullable = true; + var mapper = _dialect.GetAndMapColumnProperties(definition); + var sql = mapper.ColumnSql; + if (sql.EndsWith(" NULL", StringComparison.Ordinal)) sql = sql[..^5]; + if (existing.IsNullable != column.IsNullable) + sql += column.IsNullable ? " NULL" : " NOT NULL"; + ChangeColumn(table, sql); } private void CopyDataFromOneColumnToAnother(string table, string fromColumn, string toColumn) @@ -519,24 +481,23 @@ public override Column[] GetColumns(string table) var column = new Column(columnName, DbType.String) { - ColumnProperty = isNullable ? ColumnProperty.Null : ColumnProperty.NotNull + IsNullable = isNullable }; - if (uniqueColumns.Contains(column.Name)) column.ColumnProperty |= ColumnProperty.Unique; var isIdentity = userTabIdentityCols.Any(x => x.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)); var isPrimaryKey = primaryKeyItems.Any(x => x.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase)); if (isIdentity && isPrimaryKey) { - column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.PrimaryKeyWithIdentity); + column.IsIdentity = true; } else if (isIdentity) { - column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.Identity); + column.IsIdentity = true; } else if (isPrimaryKey) { - column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.PrimaryKey); + } // Oracle does not have unsigned types. All NUMBER types can hold positive or negative values so we do not return DbType.UIntX types. @@ -882,40 +843,12 @@ public override void RemoveColumnDefaultValue(string table, string column) public override void AddTable(string name, params IDbField[] fields) { GuardAgainstMaximumIdentifierLengthForOracle(name); - name = QuoteTableNameIfRequired(name); - - var columns = fields.Where(x => x is Column).Cast().ToArray(); - + var columns = fields.OfType().ToArray(); GuardAgainstMaximumColumnNameLengthForOracle(name, columns); - + foreach (var identity in columns.Where(c => c.IsIdentity)) + if (identity.Type is not (DbType.Int16 or DbType.Int32 or DbType.Int64 or DbType.UInt16 or DbType.UInt32 or DbType.UInt64)) + throw new MigrationException("Oracle identity columns require an integer type."); base.AddTable(name, fields); - - // Should be refactored - if (columns.Any(c => c.ColumnProperty == ColumnProperty.PrimaryKeyWithIdentity || - (c.ColumnProperty.HasFlag(ColumnProperty.Identity) && c.ColumnProperty.HasFlag(ColumnProperty.PrimaryKey)))) - { - var identityColumn = columns.First(x => x.ColumnProperty.HasFlag(ColumnProperty.Identity) && x.ColumnProperty.HasFlag(ColumnProperty.PrimaryKey)); - - List allowedIdentityDbTypes = [DbType.Int16, DbType.Int32, DbType.Int64, DbType.UInt16, DbType.UInt32, DbType.UInt64]; - - if (!allowedIdentityDbTypes.Contains(identityColumn.Type)) - { - var allowedIdentityDbTypesStringList = allowedIdentityDbTypes.Select(x => x.ToString()).ToList(); - var allowedIdentityDbTypesString = $"{string.Join(", ", allowedIdentityDbTypesStringList[..^1])} and {allowedIdentityDbTypesStringList[^1..]}"; - - throw new MigrationException($"Identity columns can only be used with {allowedIdentityDbTypesString}"); - } - - var identityColumnNameQuoted = QuoteColumnNameIfRequired(identityColumn.Name); - - using var cmd = CreateCommand(); - // We use ALWAYS in order to prevent sequence problems in cases of misuse of the column by an unexperienced user. Inserting data will result in an exception. - ExecuteQuery(cmd, $"ALTER TABLE {name} MODIFY {identityColumnNameQuoted} GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1 NOCACHE NOCYCLE)"); - } - else if (columns.Any(x => x.ColumnProperty.HasFlag(ColumnProperty.Identity) && !x.ColumnProperty.HasFlag(ColumnProperty.PrimaryKey))) - { - throw new MigrationException("Identity without Primary is currently not supported by this migrator"); - } } public override void RemoveTable(string name) diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs index 2987d1d8..2a5e6bfd 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs @@ -6,6 +6,8 @@ namespace DotNetProjects.Migrator.Providers.Impl.PostgreSQL; public class PostgreSQLDialect : Dialect { + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); + public PostgreSQLDialect() { RegisterColumnType(DbType.AnsiStringFixedLength, "char(255)"); @@ -47,7 +49,7 @@ public PostgreSQLDialect() RegisterColumnType(DbType.Guid, "uuid"); RegisterColumnType(MigratorDbType.Interval, "interval"); - RegisterProperty(ColumnProperty.Identity, "GENERATED ALWAYS AS IDENTITY"); + RegisterColumnAttribute(ColumnAttribute.Identity, "GENERATED ALWAYS AS IDENTITY"); AddReservedWords("ABS", "ABSOLUTE", "ACCESS", "ACTION", "ADA", "ADD", "ADMIN", "AFTER", "AGGREGATE", "ALIAS", "ALL", "ALLOCATE", "ALTER", "ANALYSE", "ANALYZE", "AND", "ANY", "ARE", "ARRAY", "AS", "ASC", "ASENSITIVE", "ASSERTION", "ASSIGNMENT", "ASYMMETRIC", "AT", "ATOMIC", "AUTHORIZATION", "AVG", "BACKWARD", "BEFORE", "BEGIN", "BETWEEN", "BIGINT", "BINARY", @@ -138,10 +140,10 @@ public override string Default(object defaultValue) return base.Default(defaultValue); } - //public override string SqlForProperty(ColumnProperty property, Column column) + //public override string SqlForColumnAttribute(ColumnAttribute property, Column column) //{ - // if (property == ColumnProperty.Identity && (column.Type == DbType.Int64 || column.Type == DbType.UInt32 || column.Type == DbType.UInt64)) + // if (property == ColumnAttribute.Identity && (column.Type == DbType.Int64 || column.Type == DbType.UInt32 || column.Type == DbType.UInt64)) // return "bigserial"; - // return base.SqlForProperty(property, column); + // return base.SqlForColumnAttribute(property, column); //} } diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index dd1714e3..346f9f52 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -373,9 +373,6 @@ public override void ChangeColumn(string table, Column column) { var oldColumn = GetColumnByName(table, column.Name); - var isUniqueSet = column.ColumnProperty.IsSet(ColumnProperty.Unique); - - column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.Unique); var mapper = _dialect.GetAndMapColumnProperties(column); @@ -408,7 +405,7 @@ public override void ChangeColumn(string table, Column column) ChangeColumn(table, change2); } - if (column.ColumnProperty.HasFlag(ColumnProperty.NotNull)) + if (!column.IsNullable) { var change3 = string.Format("{0} SET NOT NULL", QuoteColumnNameIfRequired(mapper.Name)); ChangeColumn(table, change3); @@ -419,10 +416,7 @@ public override void ChangeColumn(string table, Column column) ChangeColumn(table, change3); } - if (isUniqueSet) - { - AddUniqueConstraint(string.Format("UX_{0}_{1}", table, column.Name), table, [column.Name]); - } + } public override void CreateDatabases(string databaseName) @@ -609,17 +603,16 @@ public override Column[] GetColumns(string table) Size = size ?? 0 }; - column.ColumnProperty |= isNullable ? ColumnProperty.Null : ColumnProperty.NotNull; - if (uniqueColumns.Contains(column.Name)) column.ColumnProperty |= ColumnProperty.Unique; + column.IsNullable = isNullable; if (isPrimaryKey) { - column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.PrimaryKey); + } if (isIdentity) { - column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.Identity); + column.IsIdentity = true; } if (columnInfo.ColumnDefault != null) diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteColumnPropertiesMapper.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteColumnPropertiesMapper.cs index 442a7834..79fcfd2a 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteColumnPropertiesMapper.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteColumnPropertiesMapper.cs @@ -1,36 +1,2 @@ -using System.Collections.Generic; -using DotNetProjects.Migrator.Framework; - namespace DotNetProjects.Migrator.Providers.Impl.SQLite; - -public class SQLiteColumnPropertiesMapper : ColumnPropertiesMapper -{ - public SQLiteColumnPropertiesMapper(Dialect dialect, string type) : base(dialect, type) - { - } - - protected override void AddNull(Column column, List vals) - { - var isPrimaryKeySelected = PropertySelected(column.ColumnProperty, ColumnProperty.PrimaryKey); - var isNullSelected = PropertySelected(column.ColumnProperty, ColumnProperty.Null); - var isNotNullSelected = PropertySelected(column.ColumnProperty, ColumnProperty.NotNull); - - if (isNullSelected || (!isNotNullSelected && !isPrimaryKeySelected)) - { - AddValueIfSelected(column, ColumnProperty.Null, vals); - } - } - - protected override void AddNotNull(Column column, List vals) - { - if (column.ColumnProperty.HasFlag(ColumnProperty.NotNull)) - { - AddValueIfSelected(column, ColumnProperty.NotNull, vals); - } - } - - protected virtual void AddValueIfSelected(Column column, ColumnProperty property, ICollection vals) - { - vals.Add(_Dialect.SqlForProperty(property, column)); - } -} \ No newline at end of file +public class SQLiteColumnPropertiesMapper(Dialect dialect, string typeString) : ColumnPropertiesMapper(dialect, typeString); diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs index d4036da3..00ba6dd7 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs @@ -5,6 +5,8 @@ namespace DotNetProjects.Migrator.Providers.Impl.SQLite; public class SQLiteDialect : Dialect { + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); + public SQLiteDialect() { RegisterColumnType(DbType.Binary, "BINARY"); @@ -37,8 +39,7 @@ public SQLiteDialect() RegisterColumnType(DbType.Boolean, "BOOLEAN"); // Important for Dapper to know it should map to a bool RegisterColumnType(DbType.Guid, "UNIQUEIDENTIFIER"); - RegisterProperty(ColumnProperty.Identity, "AUTOINCREMENT"); - RegisterProperty(ColumnProperty.CaseSensitive, "COLLATE NOCASE"); + RegisterColumnAttribute(ColumnAttribute.Identity, "AUTOINCREMENT"); AddReservedWords("ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 511be5e2..cf0a9553 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -373,12 +373,12 @@ public override void RemoveForeignKey(string table, string name) } var sqliteTableInfo = GetSQLiteTableInfo(table); - if (!sqliteTableInfo.ForeignKeys.Any(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + if (!sqliteTableInfo.ForeignKeys.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) { throw new MigrationException($"Foreign key '{name}' does not exist."); } - sqliteTableInfo.ForeignKeys.RemoveAll(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + sqliteTableInfo.ForeignKeys.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); RecreateTable(sqliteTableInfo); } @@ -426,7 +426,7 @@ public override void RemoveColumn(string tableName, string column) var info = GetSQLiteTableInfo(tableName); var definition = info.Columns.SingleOrDefault(c => c.Name.Equals(column, StringComparison.OrdinalIgnoreCase)); bool Matches(string name) => string.Equals(name, column, StringComparison.OrdinalIgnoreCase); - var dependent = definition == null || definition.IsPrimaryKey || definition.ColumnProperty.HasFlag(ColumnProperty.Unique) + var dependent = definition == null || info.PrimaryKey?.KeyColumns.Contains(column, StringComparer.OrdinalIgnoreCase) == true || info.Uniques.Any(u => u.KeyColumns.Contains(column, StringComparer.OrdinalIgnoreCase)) || info.CheckConstraints.Count != 0 || info.Uniques.Any(u => u.KeyColumns.Any(Matches)) || info.Indexes.Any(i => i.KeyColumns.Any(Matches) || i.FilterItems.Count != 0) @@ -693,47 +693,17 @@ public override void RemoveColumnDefaultValue(string tableName, string columnNam public override void AddPrimaryKey(string name, string tableName, params string[] columnNames) { - if (!TableExists(tableName)) - { - throw new Exception("Table does not exist"); - } - - var sqliteTableInfo = GetSQLiteTableInfo(tableName); - - foreach (var column in sqliteTableInfo.Columns) - { - if (columnNames.Any(x => x.Equals(column.Name, StringComparison.OrdinalIgnoreCase))) - { - column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.PrimaryKey); - } - else - { - column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.PrimaryKey); - } - } - - var columnNamesList = columnNames.ToList(); - - var columnsReordered = sqliteTableInfo.Columns.OrderBy(x => - { - var index = columnNamesList.IndexOf(x.Name); - return index >= 0 ? index : int.MaxValue; - }).ToList(); - - sqliteTableInfo.Columns = columnsReordered; - - RecreateTable(sqliteTableInfo); + var info = GetSQLiteTableInfo(tableName) ?? throw new MigrationException("Table does not exist."); + if (info.PrimaryKey != null) throw new MigrationException("The table already has a primary key. Remove it explicitly first."); + ValidateKeyColumns(name, columnNames, info.Columns.ToArray()); + info.PrimaryKey = new PrimaryKeyConstraint(name, columnNames); + RecreateTable(info); } public override bool PrimaryKeyExists(string table, string name) { - var sqliteTableInfo = GetSQLiteTableInfo(table); - - // SQLite does not offer named primary keys BUT since there can only be one primary key per table we return true if there is any primary key. - - var hasPrimaryKey = sqliteTableInfo.Columns.Any(x => x.ColumnProperty.IsSet(ColumnProperty.PrimaryKey)); - - return hasPrimaryKey; + var key = GetTableConstraints(table).OfType().SingleOrDefault(); + return key != null && string.Equals(key.Name, name, StringComparison.OrdinalIgnoreCase); } public override void AddUniqueConstraint(string name, string table, params string[] columns) @@ -745,7 +715,7 @@ public override void AddUniqueConstraint(string name, string table, params strin var sqliteTableInfo = GetSQLiteTableInfo(table); - if (sqliteTableInfo.Uniques.Any(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + if (sqliteTableInfo.Uniques.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))) { throw new MigrationException("A unique constraint with the same name already exists."); } @@ -759,8 +729,8 @@ public override void AddUniqueConstraint(string name, string table, params strin public override void RemoveConstraint(string table, string name) { var sqliteTableInfo = GetSQLiteTableInfo(table); - sqliteTableInfo.Uniques.RemoveAll(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); - sqliteTableInfo.CheckConstraints.RemoveAll(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + sqliteTableInfo.Uniques.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + sqliteTableInfo.CheckConstraints.RemoveAll(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); RecreateTable(sqliteTableInfo); } @@ -776,7 +746,7 @@ 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), + PrimaryKey = GetTableConstraints(tableName).OfType().SingleOrDefault(), ForeignKeys = GetForeignKeyConstraints(tableName).ToList(), Indexes = GetIndexes(tableName).ToList(), Uniques = GetUniques(tableName).ToList(), @@ -902,11 +872,7 @@ private void RecreateTableCore(SQLiteTableInfo sqliteTableInfo) var targetIntermediateTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}{IntermediateTableSuffix}"); var targetTableQuoted = QuoteTableNameIfRequired($"{sqliteTableInfo.TableNameMapping.NewName}"); - // 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(); @@ -1017,28 +983,6 @@ public override void AddColumn(string table, string columnName, MigratorDbType t AddColumn(table, column); } - public override void AddColumn(string table, string columnName, DbType type, ColumnProperty property) - { - var column = new Column(columnName, type, property); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, MigratorDbType type, ColumnProperty property) - { - var column = new Column(columnName, type, property); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, MigratorDbType type, int size, ColumnProperty property, - object defaultValue) - { - var column = new Column(columnName, type, property) { Size = size, DefaultValue = defaultValue }; - - AddColumn(table, column); - } - public override void AddColumn(string table, string columnName, DbType type) { var column = new Column(columnName, type); @@ -1053,20 +997,6 @@ public override void AddColumn(string table, string columnName, MigratorDbType t AddColumn(table, column); } - public override void AddColumn(string table, string columnName, DbType type, int size, ColumnProperty property) - { - var column = new Column(columnName, type, size, property); - - AddColumn(table, column); - } - - public override void AddColumn(string table, string columnName, MigratorDbType type, int size, ColumnProperty property) - { - var column = new Column(columnName, type, size, property); - - AddColumn(table, column); - } - public override void AddColumn(string table, string columnName, DbType type, object defaultValue) { var column = new Column(columnName, type, defaultValue); @@ -1197,17 +1127,8 @@ public override Column[] GetColumns(string tableName) { var pragmaTableInfoItems = GetPragmaTableInfoItems(tableName); - // Column provides no way to store the primary key sequence number and we do not want to change the class for all database types for now - // so we sort the columns. - var tableInfoPrimaryKeys = pragmaTableInfoItems.Where(x => x.Pk > 0) - .OrderBy(x => x.Pk) - .ToList(); - - var tableInfoNonPrimaryKeys = pragmaTableInfoItems.Where(x => x.Pk < 1) - .OrderBy(x => x.Cid) - .ToList(); - - var pragmaTableInfoItemsSorted = tableInfoPrimaryKeys.Concat(tableInfoNonPrimaryKeys).ToList(); + var tableInfoPrimaryKeys = pragmaTableInfoItems.Where(x => x.Pk > 0).ToList(); + var pragmaTableInfoItemsSorted = pragmaTableInfoItems.OrderBy(x => x.Cid).ToList(); var columns = new List(); @@ -1220,11 +1141,11 @@ public override Column[] GetColumns(string tableName) if (pragmaTableInfoItem.NotNull) { - column.ColumnProperty |= ColumnProperty.NotNull; + column.IsNullable = false; } else { - column.ColumnProperty |= ColumnProperty.Null; + column.IsNullable = true; } var defValue = pragmaTableInfoItem.DfltValue == DBNull.Value ? null : pragmaTableInfoItem.DfltValue; @@ -1292,35 +1213,6 @@ public override Column[] GetColumns(string tableName) } } - if (pragmaTableInfoItem.Pk > 0) - { - if (new[] { DbType.UInt16, DbType.UInt32, DbType.UInt64, DbType.Int16, DbType.Int32, DbType.Int64 }.Contains(column.Type)) - { - column.ColumnProperty |= ColumnProperty.PrimaryKey; - column.ColumnProperty |= ColumnProperty.NotNull; - column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.Null); - } - else - { - column.ColumnProperty |= ColumnProperty.PrimaryKey; - } - } - - var indexListItems = GetPragmaIndexListItems(tableName); - var uniqueConstraints = indexListItems.Where(x => x.Unique && x.Origin == "u"); - - foreach (var uniqueConstraint in uniqueConstraints) - { - var indexInfos = GetPragmaIndexInfo(uniqueConstraint.Name); - - if (indexInfos.Count == 1 && indexInfos.First().Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)) - { - column.ColumnProperty |= ColumnProperty.Unique; - - break; - } - } - var tableScript = GetSqlCreateTableScript(tableName); var columnTableInfoItem = pragmaTableInfoItems.First(x => x.Name.Equals(column.Name, StringComparison.OrdinalIgnoreCase)); @@ -1328,9 +1220,9 @@ public override Column[] GetColumns(string tableName) var hasCompoundPrimaryKey = tableInfoPrimaryKeys.Count > 1; // Implicit in SQLite - if (columnTableInfoItem.Type == "INTEGER" && columnTableInfoItem.Pk == 1 && !hasCompoundPrimaryKey) + if (columnTableInfoItem.Type == "INTEGER" && columnTableInfoItem.Pk == 1 && !hasCompoundPrimaryKey && Regex.IsMatch(tableScript, @"\bAUTOINCREMENT\b", RegexOptions.IgnoreCase)) { - column.ColumnProperty |= ColumnProperty.Identity; + column.IsIdentity = true; } columns.Add(column); @@ -1473,62 +1365,29 @@ public override void AddTable(string name, string engine, params IDbField[] fiel { 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; + foreach (var column in columns.Where(c => explicitKey.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) + column.IsNullable = false; 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")) + if (identities.Length != 0 && (identities.Length != 1 || explicitKey.KeyColumns.Length != 1 || !explicitKey.KeyColumns[0].Equals(identities[0].Name, StringComparison.OrdinalIgnoreCase) || _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; - - var columnProviders = new List(columns.Length); - - foreach (var column in columns) + if (explicitKey == null && columns.Any(c => c.IsIdentity)) + throw new MigrationException("SQLite identity requires an explicit INTEGER primary-key constraint."); + var columnSql = columns.Select(column => { - if (!hasCompoundPrimaryKey && column.IsPrimaryKey) - { - // We implicitly set NOT NULL for non-composite primary keys like in other RDBMS. - column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.Null); - column.ColumnProperty = column.ColumnProperty.Set(ColumnProperty.NotNull); - } - - if (hasCompoundPrimaryKey && column.IsPrimaryKey) - { - // We remove PrimaryKey here and readd it as compound later ("...PRIMARY KEY(column1,column2)"); - column.ColumnProperty &= ~ColumnProperty.PrimaryKey; - - // AUTOINCREMENT cannot be used in compound primary keys in SQLite so we remove Identity here - column.ColumnProperty &= ~ColumnProperty.Identity; - } - var mapped = column.CopyDefinition(); - if (explicitKey != null && mapped.IsIdentity) mapped.ColumnProperty &= ~ColumnProperty.Identity; - var mapper = _dialect.GetAndMapColumnProperties(mapped); - columnProviders.Add(mapper); - } - - 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); + mapped.IsIdentity = false; + var sql = _dialect.GetAndMapColumnProperties(mapped).ColumnSql; + if (column.IsIdentity) + sql += (explicitKey.Name == null ? "" : $" CONSTRAINT {_dialect.QuoteIdentifier(explicitKey.Name)}") + " PRIMARY KEY AUTOINCREMENT"; + return sql; + }).ToList(); if (explicitKey != null && !columns.Any(c => c.IsIdentity)) - columnsAndIndexes += ", " + _dialect.GetTableConstraintSql(explicitKey); - + columnSql.Add(_dialect.GetTableConstraintSql(explicitKey)); var table = _dialect.TableNameNeedsQuote ? _dialect.Quote(name) : QuoteTableNameIfRequired(name); - StringBuilder stringBuilder = new(); - - stringBuilder.Append(string.Format("CREATE TABLE {0} ({1}", table, columnsAndIndexes)); - - if (hasCompoundPrimaryKey) - { - stringBuilder.Append(string.Format(", PRIMARY KEY ({0})", string.Join(", ", pks.ToArray()))); - } - + var stringBuilder = new StringBuilder($"CREATE TABLE {table} ({string.Join(", ", columnSql)}"); // Uniques var uniques = fields.Where(x => x is UniqueConstraint).Cast().ToArray(); @@ -1559,12 +1418,9 @@ public override void AddTable(string name, string engine, params IDbField[] fiel var parentColumnNamesQuotedString = string.Join(", ", fk.ParentColumns.Select(QuoteColumnNameIfRequired)); var parentTableNameQuoted = QuoteTableNameIfRequired(fk.ParentTable); - if (string.IsNullOrWhiteSpace(fk.Name)) - { - throw new Exception("No foreign key constraint name given"); - } - - var foreignKeySql = $"CONSTRAINT {QuoteConstraintNameIfRequired(fk.Name)} FOREIGN KEY ({sourceColumnNamesQuotedString}) REFERENCES {parentTableNameQuoted}({parentColumnNamesQuotedString})"; + var foreignKeySql = (fk.Name == null ? "" : $"CONSTRAINT {_dialect.QuoteIdentifier(fk.Name)} ") + + $"FOREIGN KEY ({sourceColumnNamesQuotedString}) REFERENCES {parentTableNameQuoted}" + + (fk.ParentColumns.Length == 0 ? "" : $"({parentColumnNamesQuotedString})"); if (!string.IsNullOrWhiteSpace(fk.OnDelete) && !string.Equals(fk.OnDelete, "NO ACTION", StringComparison.OrdinalIgnoreCase)) { foreignKeySql += $" ON DELETE {ValidateForeignKeyAction(fk.OnDelete)}"; @@ -1586,7 +1442,7 @@ public override void AddTable(string name, string engine, params IDbField[] fiel foreach (var checkConstraint in checkConstraints) { - checkConstraintStrings.Add($"CONSTRAINT {QuoteConstraintNameIfRequired(checkConstraint.Name)} CHECK ({checkConstraint.CheckConstraintString})"); + checkConstraintStrings.Add(_dialect.GetTableConstraintSql(checkConstraint)); } if (checkConstraintStrings.Count > 0) @@ -1677,50 +1533,27 @@ public override string AddIndex(string table, Index index) protected override string GetPrimaryKeyConstraintName(string table) { - throw new NotImplementedException(); + return GetTableConstraints(table).OfType().SingleOrDefault()?.Name; } public override void RemoveAllConstraints(string table) { - RemovePrimaryKey(table); - - var sqliteTableInfo = GetSQLiteTableInfo(table); - - // Remove unique constraints - sqliteTableInfo.Uniques = []; - - foreach (var column in sqliteTableInfo.Columns) - { - column.ColumnProperty &= ~ColumnProperty.PrimaryKey; - column.ColumnProperty &= ~ColumnProperty.Unique; - } - - sqliteTableInfo.ForeignKeys.Clear(); - sqliteTableInfo.CheckConstraints.Clear(); - - RecreateTable(sqliteTableInfo); + var info = GetSQLiteTableInfo(table); + info.PrimaryKey = null; + info.Uniques.Clear(); + info.ForeignKeys.Clear(); + info.CheckConstraints.Clear(); + foreach (var column in info.Columns) column.IsIdentity = false; + RecreateTable(info); } public override void RemovePrimaryKey(string tableName) { - if (!TableExists(tableName)) - { - return; - } - - var sqliteInfoTable = GetSQLiteTableInfo(tableName); - sqliteInfoTable.PrimaryKey = null; - - foreach (var column in sqliteInfoTable.Columns) - { - if (column.IsPrimaryKey) - { - column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.PrimaryKey); - column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.PrimaryKeyWithIdentity); - } - } - - RecreateTable(sqliteInfoTable); + if (!TableExists(tableName)) return; + var info = GetSQLiteTableInfo(tableName); + info.PrimaryKey = null; + foreach (var column in info.Columns) column.IsIdentity = false; + RecreateTable(info); } public override void RemoveAllIndexes(string tableName) @@ -1732,14 +1565,13 @@ public override void RemoveAllIndexes(string tableName) var sqliteInfoTable = GetSQLiteTableInfo(tableName); - sqliteInfoTable.Uniques = []; sqliteInfoTable.Indexes = []; RecreateTable(sqliteInfoTable); } public List GetUniques(string tableName) => GetTableConstraints(tableName) - .OfType().Where(c => c.Name != null).ToList(); + .OfType().ToList(); public List GetPragmaIndexInfo(string indexNameNotQuoted) { diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs index 3968c1e4..16adc353 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs @@ -6,6 +6,10 @@ namespace DotNetProjects.Migrator.Providers.Impl.SqlServer; public class SqlServerDialect : Dialect { + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); + + public override bool NeedsNullForNullableWhenAlteringTable => true; + public const string DboSchemaName = "dbo"; public SqlServerDialect() @@ -53,7 +57,7 @@ public SqlServerDialect() RegisterColumnType(DbType.VarNumeric, 38, "NUMERIC($l,0)"); RegisterColumnType(MigratorDbType.Interval, "BIGINT"); - RegisterProperty(ColumnProperty.Identity, "IDENTITY"); + RegisterColumnAttribute(ColumnAttribute.Identity, "IDENTITY"); AddReservedWords("ADD", "EXCEPT", "PERCENT", "ALL", "EXEC", "PLAN", "ALTER", "EXECUTE", "PRECISION", "AND", "EXISTS", "PRIMARY", "ANY", "EXIT", "PRINT", "AS", "FETCH", "PROC", "ASC", "FILE", "PROCEDURE", "AUTHORIZATION", "FILLFACTOR", "PUBLIC", "BACKUP", "FOR", "RAISERROR", "BEGIN", "FOREIGN", "READ", "BETWEEN", "FREETEXT", "READTEXT", "BREAK", "FREETEXTTABLE", "RECONFIGURE", "BROWSE", "FROM", "REFERENCES", "BULK", "FULL", "REPLICATION", "BY", "FUNCTION", "RESTORE", "CASCADE", "GOTO", "RESTRICT", "CASE", "GRANT", "RETURN", "CHECK", "GROUP", "REVOKE", "CHECKPOINT", "HAVING", "RIGHT", "CLOSE", "HOLDLOCK", "ROLLBACK", "CLUSTERED", "IDENTITY", "ROWCOUNT", "COALESCE", "IDENTITY_INSERT", "ROWGUIDCOL", "COLLATE", "IDENTITYCOL", "RULE", "COLUMN", "IF", "SAVE", "COMMIT", "IN", "SCHEMA", "COMPUTE", "INDEX", "SELECT", "CONSTRAINT", "INNER", "SESSION_USER", "CONTAINS", "INSERT", "SET", "CONTAINSTABLE", "INTERSECT", "SETUSER", "CONTINUE", "INTO", "SHUTDOWN", "CONVERT", "IS", "SOME", "CREATE", "JOIN", "STATISTICS", "CROSS", "KEY", "SYSTEM_USER", "CURRENT", "KILL", "TABLE", "CURRENT_DATE", "LEFT", "TEXTSIZE", "CURRENT_TIME", "LIKE", "THEN", "CURRENT_TIMESTAMP", "LINENO", "TO", "CURRENT_USER", "LOAD", "TOP", "CURSOR", "NATIONAL", "TRAN", "DATABASE", "NOCHECK", "TRANSACTION", "DBCC", "NONCLUSTERED", "TRIGGER", "DEALLOCATE", "NOT", "TRUNCATE", "DECLARE", "NULL", "TSEQUAL", "DEFAULT", "NULLIF", "UNION", "DELETE", "OF", "UNIQUE", "DENY", "OFF", "UPDATE", "DESC", "OFFSETS", "UPDATETEXT", "DISK", "ON", "USE", "DISTINCT", "OPEN", "USER", "DISTRIBUTED", "OPENDATASOURCE", "VALUES", "DOUBLE", "OPENQUERY", "VARYING", "DROP", "OPENROWSET", "VIEW", "DUMMY", "OPENXML", "WAITFOR", "DUMP", "OPTION", "WHEN", "ELSE", "OR", "WHERE", "END", "ORDER", "WHILE", "ERRLVL", "OUTER", "WITH", "ESCAPE", "OVER", "WRITETEXT"); } diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 805d5fbd..6f4ab238 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -55,20 +55,7 @@ protected virtual void CreateConnection(string providerName) _connection.ConnectionString = _connectionString; _connection.Open(); - string collationString = null; - var collation = ExecuteScalar("SELECT DATABASEPROPERTYEX('" + _connection.Database + "', 'Collation')"); - if (collation != null) - { - collationString = collation.ToString(); - } - - if (string.IsNullOrWhiteSpace(collationString)) - { - collationString = "Latin1_General_CI_AS"; - } - - Dialect.RegisterProperty(ColumnProperty.CaseSensitive, "COLLATE " + collationString.Replace("_CI_", "_CS_")); } public override void CopyDataFromTableToTable(string sourceTableName, List sourceColumnNames, string targetTableName, List targetColumnNames, List orderBySourceColumns = null) @@ -277,78 +264,15 @@ public override string AddIndex(string table, Index index) return sql; } - public override void AddTable(string name, string engine, params IDbField[] fields) - { - var definitions = fields.Select(field => field is Column column ? column.CopyDefinition() : field).ToArray(); - var owned = definitions.OfType().Where(c => c.ColumnProperty.HasFlag(ColumnProperty.Unique)).ToArray(); - foreach (var column in owned) column.ColumnProperty &= ~ColumnProperty.Unique; - base.AddTable(name, engine, definitions); - foreach (var column in owned) AddOwnedColumnUnique(name, column.Name); - } - - public override void AddColumn(string table, Column column) - { - var definition = column.CopyDefinition(); - var owned = definition.ColumnProperty.HasFlag(ColumnProperty.Unique); - definition.ColumnProperty &= ~ColumnProperty.Unique; - base.AddColumn(table, definition); - if (owned) AddOwnedColumnUnique(table, column.Name); - } - - public override void AddColumn(string table, string column, MigratorDbType type, int size, ColumnProperty property, object defaultValue) - { - base.AddColumn(table, column, type, size, property & ~ColumnProperty.Unique, defaultValue); - if (property.HasFlag(ColumnProperty.Unique)) AddOwnedColumnUnique(table, column); - } - - private void AddOwnedColumnUnique(string table, string column) - { - var name = "UX_" + Guid.NewGuid().ToString("N"); - AddUniqueConstraint(name, table, column); - MarkColumnUniqueOwned(table, column, name); - } - - /// Explicitly adopt a caller-owned, single-column legacy UNIQUE constraint. - /// No ownership is inferred from its name. Future ChangeColumn calls may remove it. - public void AdoptColumnUniqueConstraint(string table, string column, string constraint) - { - using var command = CreateCommand(); - command.CommandText = "SELECT COUNT(*) 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 kc.type='UQ' AND kc.name=@constraint AND ic.key_ordinal=1 AND c.name=@column AND NOT EXISTS (SELECT 1 FROM sys.index_columns more WHERE more.object_id=ic.object_id AND more.index_id=ic.index_id AND more.key_ordinal>1)"; - AddParameter(command, "@table", table); AddParameter(command, "@column", column); AddParameter(command, "@constraint", constraint); - if (Convert.ToInt32(command.ExecuteScalar()) != 1) throw new MigrationException("Ownership requires an existing single-column UNIQUE constraint on the specified table and column."); - MarkColumnUniqueOwned(table, column, constraint); - } - - private void MarkColumnUniqueOwned(string table, string column, string constraint) - { - using var command = CreateCommand(); - command.CommandText = "DECLARE @schema sysname=OBJECT_SCHEMA_NAME(OBJECT_ID(@table)); DECLARE @name sysname=OBJECT_NAME(OBJECT_ID(@table)); IF EXISTS (SELECT 1 FROM sys.extended_properties ep JOIN sys.key_constraints kc ON ep.class=1 AND ep.major_id=kc.object_id AND ep.minor_id=0 WHERE kc.parent_object_id=OBJECT_ID(@table) AND kc.name=@constraint AND ep.name=N'Migrator.NET.ColumnUnique') EXEC sys.sp_updateextendedproperty @name=N'Migrator.NET.ColumnUnique', @value=@column, @level0type=N'SCHEMA', @level0name=@schema, @level1type=N'TABLE', @level1name=@name, @level2type=N'CONSTRAINT', @level2name=@constraint; ELSE EXEC sys.sp_addextendedproperty @name=N'Migrator.NET.ColumnUnique', @value=@column, @level0type=N'SCHEMA', @level0name=@schema, @level1type=N'TABLE', @level1name=@name, @level2type=N'CONSTRAINT', @level2name=@constraint"; - AddParameter(command, "@table", table); AddParameter(command, "@column", column); AddParameter(command, "@constraint", constraint); - command.ExecuteNonQuery(); - } - public override void ChangeColumn(string table, Column column) { - var definition = new Column(column.Name, column.MigratorDbType, column.Size, column.ColumnProperty, column.DefaultValue) - { Precision = column.Precision, Scale = column.Scale }; - var unique = definition.ColumnProperty.IsSet(ColumnProperty.Unique); - definition.ColumnProperty = definition.ColumnProperty.Clear(ColumnProperty.Unique); - var owned = new List(); - using (var command = CreateCommand()) - { - command.CommandText = "SELECT kc.name FROM sys.key_constraints kc JOIN sys.extended_properties ep ON ep.class=1 AND ep.major_id=kc.object_id AND ep.minor_id=0 WHERE kc.parent_object_id=OBJECT_ID(@table) AND kc.type='UQ' AND ep.name=N'Migrator.NET.ColumnUnique' AND CONVERT(nvarchar(128),ep.value)=@column"; - AddParameter(command, "@table", table); AddParameter(command, "@column", column.Name); - using var reader = command.ExecuteReader(); - while (reader.Read()) owned.Add(reader.GetString(0)); - } - foreach (var constraint in owned) RemoveConstraint(table, constraint); + var definition = column.CopyDefinition(); RemoveColumnDefaultValue(table, definition.Name); var requestedDefault = definition.DefaultValue; definition.DefaultValue = null; base.ChangeColumn(table, definition); if (requestedDefault != null && requestedDefault != DBNull.Value) ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ADD DEFAULT {_dialect.Default(requestedDefault)[8..]} FOR {QuoteColumnNameIfRequired(column.Name)}"); - if (unique) AddOwnedColumnUnique(table, column.Name); } private static void AddParameter(IDbCommand command, string name, object value) @@ -626,15 +550,14 @@ public override Column[] GetColumns(string table) var defaultValueString = reader.IsDBNull(defaultValueOrdinal) ? null : reader.GetString(defaultValueOrdinal).Trim(); var characterMaximumLength = reader.IsDBNull(characterMaximumLengthOrdinal) ? (int?)null : reader.GetInt32(characterMaximumLengthOrdinal); - if (uniqueColumns.Contains(column.Name)) column.ColumnProperty |= ColumnProperty.Unique; if (pkColumns.Contains(column.Name)) { - column.ColumnProperty |= ColumnProperty.PrimaryKey; + } if (idtColumns.Contains(column.Name)) { - column.ColumnProperty |= ColumnProperty.Identity; + column.IsIdentity = true; } var nullableStr = reader.GetString(1); @@ -842,7 +765,7 @@ public override Column[] GetColumns(string table) } } - column.ColumnProperty |= isNullable ? ColumnProperty.Null : ColumnProperty.NotNull; + column.IsNullable = isNullable; columns.Add(column); } diff --git a/src/Migrator/Providers/Impl/Sybase/SybaseDialect.cs b/src/Migrator/Providers/Impl/Sybase/SybaseDialect.cs index 8d76f09d..54a64516 100644 --- a/src/Migrator/Providers/Impl/Sybase/SybaseDialect.cs +++ b/src/Migrator/Providers/Impl/Sybase/SybaseDialect.cs @@ -37,7 +37,7 @@ public SybaseDialect() RegisterColumnType(DbType.StringFixedLength, 255, "CHAR($l)"); RegisterColumnType(DbType.AnsiStringFixedLength, "CHAR(255)"); RegisterColumnType(DbType.AnsiStringFixedLength, 255, "CHAR($l)"); - RegisterProperty(ColumnProperty.Identity, "IDENTITY"); + RegisterColumnAttribute(ColumnAttribute.Identity, "IDENTITY"); } public override string Default(object value) => value is bool boolean ? (boolean ? "DEFAULT 1" : "DEFAULT 0") : base.Default(value); @@ -58,10 +58,12 @@ private sealed class NativeColumnMapper(Dialect dialect, string type) : ColumnPr public override void MapColumnProperties(Column column) { Name = column.Name; - _Indexed = PropertySelected(column.ColumnProperty, ColumnProperty.Indexed); + var parts = new System.Collections.Generic.List(); AddName(parts); AddType(parts); + AddCollation(column, parts); + AddUnsigned(column, parts); AddDefaultValue(column, parts); if (column.IsIdentity) AddIdentityAgain(column, parts); else @@ -69,8 +71,7 @@ public override void MapColumnProperties(Column column) AddNotNull(column, parts); AddNull(column, parts); } - AddPrimaryKey(column, parts); - AddUnique(column, parts); + _ColumnSql = string.Join(" ", parts); } } diff --git a/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs b/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs index 7474054d..96e9b7a4 100644 --- a/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs @@ -27,8 +27,6 @@ public override void AddColumn(string table, Column column) => public override void AddTable(string name, string engine, params IDbField[] fields) { base.AddTable(name, engine, fields); - foreach (var column in fields.OfType().Where(c => c.ColumnProperty.HasFlag(ColumnProperty.Indexed))) - AddIndex(name, new Index { KeyColumns = [column.Name] }); } public override bool TableExists(string table) => Convert.ToInt32(ExecuteScalar( @@ -67,7 +65,7 @@ public override Column[] GetColumns(string table) var status = Convert.ToInt32(reader.GetValue(2)); var column = new Column(reader.GetString(0), type) { - ColumnProperty = (status & 8) != 0 ? ColumnProperty.Null : ColumnProperty.NotNull + IsNullable = (status & 8) != 0 }; if (type == DbType.Decimal) { @@ -75,14 +73,35 @@ public override Column[] GetColumns(string table) if (!reader.IsDBNull(5)) column.Scale = Convert.ToInt32(reader.GetValue(5)); } if (defaults.TryGetValue(column.Name, out var defaultSql)) column.DefaultValue = CatalogDefaultValue.Parse(defaultSql, type); - if ((status & 128) != 0) column.ColumnProperty |= ColumnProperty.Identity; + if ((status & 128) != 0) column.IsIdentity = true; if (type == DbType.String) column.Size = nativeType is "text" or "unitext" ? int.MaxValue : Convert.ToInt32(reader.GetValue(3)); - if (primaryColumns.Contains(column.Name)) column.ColumnProperty |= ColumnProperty.PrimaryKey; columns.Add(column); } return columns.ToArray(); } + public override TableConstraint[] GetTableConstraints(string table) + { + var constraints = new List(); + foreach (var index in GetIndexes(table)) + { + if (index.PrimaryKey) constraints.Add(new PrimaryKeyConstraint(index.Name, index.KeyColumns) { NonClustered = !index.Clustered }); + else if (index.UniqueConstraint) constraints.Add(new DotNetProjects.Migrator.Framework.UniqueConstraint(index.Name, index.KeyColumns)); + } + var checks = new Dictionary(); + using (var command = CreateCommand()) + using (var reader = ExecuteQuery(command, $"SELECT o.name,c.text FROM sysconstraints con JOIN sysobjects o ON o.id=con.constrid JOIN syscomments c ON c.id=o.id WHERE con.tableid=object_id('{Literal(table)}') AND o.type='C' ORDER BY o.name,c.colid2,c.colid")) + while (reader.Read()) + { + var name = reader.GetString(0); + if (!checks.TryGetValue(name, out var text)) checks[name] = text = new System.Text.StringBuilder(); + text.Append(reader.GetString(1)); + } + constraints.AddRange(checks.Select(c => new CheckConstraint(c.Key, ConstraintMetadataReader.CheckExpression(c.Value.ToString())))); + constraints.AddRange(GetForeignKeyConstraints(table)); + return constraints.ToArray(); + } + private Dictionary GetColumnDefaults(string table) { var defaults = new Dictionary(); @@ -156,14 +175,11 @@ public override void RenameTable(string oldName, string newName) => public override void RemoveColumnDefaultValue(string table, string column) => ExecuteNonQuery($"ALTER TABLE {table} REPLACE {column} DEFAULT NULL"); public override void ChangeColumn(string table, Column column) { - var isUniqueSet = column.ColumnProperty.HasFlag(ColumnProperty.Unique); - column.ColumnProperty &= ~ColumnProperty.Unique; + var type = _dialect.GetColumnMapper(column).Type; - var nullable = column.ColumnProperty.HasFlag(ColumnProperty.NotNull) ? "NOT NULL" : "NULL"; + var nullable = !column.IsNullable ? "NOT NULL" : "NULL"; ExecuteNonQuery($"ALTER TABLE {table} MODIFY {column.Name} {type} {nullable}"); ExecuteNonQuery($"ALTER TABLE {table} REPLACE {column.Name} {(column.DefaultValue == null ? "DEFAULT NULL" : _dialect.Default(column.DefaultValue))}"); - if (isUniqueSet) - AddUniqueConstraint($"UX_{table}_{column.Name}", table, [column.Name]); } public override void AddForeignKey(string name, string childTable, string[] childColumns, string parentTable, string[] parentColumns, diff --git a/src/Migrator/Providers/NoOpTransformationProvider.cs b/src/Migrator/Providers/NoOpTransformationProvider.cs index a808049f..b81592b2 100644 --- a/src/Migrator/Providers/NoOpTransformationProvider.cs +++ b/src/Migrator/Providers/NoOpTransformationProvider.cs @@ -3,7 +3,7 @@ using System.Data; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Framework.Models; -using DotNetProjects.Migrator.Framework.SchemaBuilder; + using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; using Index = DotNetProjects.Migrator.Framework.Index; @@ -165,11 +165,6 @@ public bool ViewExists(string view) return false; } - public void AddColumn(string table, string column, DbType type, int size, ColumnProperty property, object defaultValue) - { - // No Op - } - public void AddColumn(string table, string column, DbType type) { // No Op @@ -185,16 +180,6 @@ public void AddColumn(string table, string column, DbType type, int size) // No Op } - public void AddColumn(string table, string column, DbType type, ColumnProperty property) - { - // No Op - } - - public void AddColumn(string table, string column, DbType type, int size, ColumnProperty property) - { - // No Op - } - public void AddPrimaryKey(string name, string table, params string[] columns) { // No Op @@ -417,11 +402,6 @@ public IDbCommand GetCommand() return null; } - public void ExecuteSchemaBuilder(SchemaBuilder schemaBuilder) - { - // No Op - } - public void RemoveAllForeignKeys(string tableName, string columnName) { @@ -565,11 +545,6 @@ public int GetColumnContentSize(string table, string columnName) throw new NotImplementedException(); } - public void AddColumn(string table, string column, MigratorDbType type, int size, ColumnProperty property, object defaultValue) - { - throw new NotImplementedException(); - } - public void AddColumn(string table, string column, MigratorDbType type) { throw new NotImplementedException(); @@ -580,16 +555,6 @@ public void AddColumn(string table, string column, MigratorDbType type, int size throw new NotImplementedException(); } - public void AddColumn(string table, string column, MigratorDbType type, int size, ColumnProperty property) - { - throw new NotImplementedException(); - } - - public void AddColumn(string table, string column, MigratorDbType type, ColumnProperty property) - { - throw new NotImplementedException(); - } - public void AddColumn(string table, string column, MigratorDbType type, object defaultValue) { throw new NotImplementedException(); diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index a7d21719..db1603a2 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -14,7 +14,7 @@ using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Framework.Loggers; using DotNetProjects.Migrator.Framework.Models; -using DotNetProjects.Migrator.Framework.SchemaBuilder; + using DotNetProjects.Migrator.Providers.Impl.SQLite; using DotNetProjects.Migrator.Providers.Models; using System; @@ -133,7 +133,7 @@ public virtual Column[] GetColumns(string table) var column = new Column(reader.GetString(0), DbType.String); var nullableStr = reader.GetString(1); var isNullable = nullableStr == "YES"; - column.ColumnProperty |= isNullable ? ColumnProperty.Null : ColumnProperty.NotNull; + column.IsNullable = isNullable; columns.Add(column); } @@ -403,65 +403,25 @@ public virtual void AddTable(string name, params IDbField[] columns) public virtual void AddTable(string name, string engine, params IDbField[] fields) { 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) + var keys = fields.OfType().ToArray(); + if (keys.Length > 1) throw new MigrationException("A table can have only one primary key."); + foreach (var key in keys) { - 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; + ValidateKeyColumns(key.Name, key.KeyColumns, columns); + foreach (var column in columns.Where(c => key.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) + column.IsNullable = false; } foreach (var unique in fields.OfType()) ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); - - var pks = GetPrimaryKeys(columns); - var compoundPrimaryKey = pks.Count > 1; - - var columnProviders = new List(columns.Count()); - - foreach (var column in columns) - { - // Remove the primary key notation if compound primary key because we'll add it back later - if (compoundPrimaryKey && column.IsPrimaryKey) - { - column.ColumnProperty = column.ColumnProperty ^ ColumnProperty.PrimaryKey; - column.ColumnProperty = column.ColumnProperty | ColumnProperty.NotNull; // PK is always not-null - } - - var mapper = _dialect.GetAndMapColumnProperties(column); - columnProviders.Add(mapper); - } - - var columnsAndIndexes = JoinColumnsAndIndexes(columnProviders); - foreach (var constraint in fields.OfType().Where(c => c is not ForeignKeyConstraint)) - columnsAndIndexes += ", " + Dialect.GetTableConstraintSql(constraint); - - AddTable(name, engine, columnsAndIndexes); - - if (compoundPrimaryKey) - { - AddPrimaryKey(GetPrimaryKeyname(name), name, pks.ToArray()); - } - - var indexes = fields.Where(x => x is Index).Cast().ToArray(); - - foreach (var index in indexes) - { - AddIndex(name, index); - } - - var foreignKeys = fields.Where(x => x is ForeignKeyConstraint).Cast().ToArray(); - - foreach (var foreignKey in foreignKeys) - { - AddForeignKey(name, foreignKey); - } + var sql = columns.Select(c => _dialect.GetAndMapColumnProperties(c).ColumnSql) + .Concat(fields.OfType().Where(c => c is not ForeignKeyConstraint).Select(_dialect.GetTableConstraintSql)); + AddTable(name, engine, string.Join(", ", sql)); + foreach (var index in fields.OfType()) AddIndex(name, index); + foreach (var foreignKey in fields.OfType()) AddForeignKey(name, foreignKey); } protected static void ValidateKeyColumns(string name, string[] keys, Column[] columns) { - if (string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name is required."); + if (name != null && string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name must not be empty."); 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)))) @@ -555,18 +515,13 @@ public virtual bool ColumnExists(string table, string column, bool ignoreCase) public virtual void ChangeColumn(string table, Column column) { column = column.CopyDefinition(); - var isUniqueSet = column.ColumnProperty.IsSet(ColumnProperty.Unique); - column.ColumnProperty = column.ColumnProperty.Clear(ColumnProperty.Unique); var mapper = _dialect.GetAndMapColumnProperties(column); ChangeColumn(table, mapper.ColumnSql); - if (isUniqueSet) - { - AddUniqueConstraint(string.Format("UX_{0}_{1}", table, column.Name), table, [column.Name]); - } + } public virtual void RemoveColumnDefaultValue(string table, string column) @@ -612,77 +567,24 @@ public virtual void DropDatabases(string databaseName) ExecuteNonQuery(string.Format("DROP DATABASE {0}", databaseName)); } - /// - /// Add a new column to an existing table. - /// - /// Table to which to add the column - /// Column name - /// Date type of the column - /// Max length of the column - /// Properties of the column, see ColumnProperty, - /// Default value - public void AddColumn(string table, string column, DbType type, int size, ColumnProperty property, - object defaultValue) - { - AddColumn(table, column, (MigratorDbType)type, size, property, defaultValue); - } - - /// - /// Add a new column to an existing table. - /// - /// Table to which to add the column - /// Column name - /// Date type of the column - /// Max length of the column - /// Properties of the column, see ColumnProperty, - /// Default value - public virtual void AddColumn(string table, string column, MigratorDbType type, int size, ColumnProperty property, - object defaultValue) - { - var mapper = - _dialect.GetAndMapColumnProperties(new Column(column, type, size, property, defaultValue)); - - AddColumn(table, mapper.ColumnSql); - } - - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// public virtual void AddColumn(string table, string column, DbType type) { - AddColumn(table, column, type, 0, ColumnProperty.Null, null); + AddColumn(table, new Column(column, type)); } - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// public virtual void AddColumn(string table, string column, MigratorDbType type) { - AddColumn(table, column, type, 0, ColumnProperty.Null, null); + AddColumn(table, new Column(column, type)); } - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// public virtual void AddColumn(string table, string column, DbType type, int size) { - AddColumn(table, column, type, size, ColumnProperty.Null, null); + AddColumn(table, new Column(column, type, size)); } - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// public virtual void AddColumn(string table, string column, MigratorDbType type, int size) { - AddColumn(table, column, type, size, ColumnProperty.Null, null); + AddColumn(table, new Column(column, type, size)); } public virtual void AddColumn(string table, string column, DbType type, object defaultValue) @@ -698,46 +600,6 @@ public virtual void AddColumn(string table, string column, MigratorDbType type, AddColumn(table, mapper.ColumnSql); } - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// - public virtual void AddColumn(string table, string column, DbType type, ColumnProperty property) - { - AddColumn(table, column, type, 0, property, null); - } - - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// - public virtual void AddColumn(string table, string column, MigratorDbType type, ColumnProperty property) - { - AddColumn(table, column, type, 0, property, null); - } - - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// - public virtual void AddColumn(string table, string column, DbType type, int size, ColumnProperty property) - { - AddColumn(table, column, type, size, property, null); - } - - /// - /// - /// AddColumn(string, string, Type, int, ColumnProperty, object) - /// - /// - public virtual void AddColumn(string table, string column, MigratorDbType type, int size, ColumnProperty property) - { - AddColumn(table, column, type, size, property, null); - } - /// /// Append a primary key to a table. /// @@ -1701,14 +1563,7 @@ public virtual void MigrationUnApplied(long version, string scope) public virtual void AddColumn(string table, Column column) { - if (!column.Precision.HasValue && !column.Scale.HasValue) - { - AddColumn(table, column.Name, column.Type, column.Size, column.ColumnProperty, column.DefaultValue); - return; - } - var definition = new Column(column.Name, column.MigratorDbType, column.Size, column.ColumnProperty, column.DefaultValue) - { Precision = column.Precision, Scale = column.Scale }; - AddColumn(table, _dialect.GetAndMapColumnProperties(definition).ColumnSql); + AddColumn(table, _dialect.GetAndMapColumnProperties(column.CopyDefinition()).ColumnSql); } public virtual void GenerateForeignKey(string primaryTable, string refTable) @@ -1726,14 +1581,6 @@ public virtual IDbCommand GetCommand() return BuildCommand(null); } - public virtual void ExecuteSchemaBuilder(SchemaBuilder builder) - { - foreach (var expr in builder.Expressions) - { - expr.Create(this); - } - } - public void Dispose() { try { if (_transaction != null) Rollback(); } @@ -1799,20 +1646,7 @@ public virtual void AddTable(string table, string engine, string columns) ExecuteNonQuery(sqlCreate); } - public virtual List GetPrimaryKeys(IEnumerable columns) - { - var primaryKeys = new List(); - - foreach (var col in columns) - { - if (col.IsPrimaryKey) - { - primaryKeys.Add(col.Name); - } - } - return primaryKeys; - } public virtual void AddColumnDefaultValue(string table, string column, object defaultValue) { @@ -1842,34 +1676,6 @@ public virtual void ChangeColumn(string table, string sqlColumn) ExecuteNonQuery(string.Format("ALTER TABLE {0} ALTER COLUMN {1}", table, sqlColumn)); } - protected virtual string JoinColumnsAndIndexes(IEnumerable columns) - { - var indexes = JoinIndexes(columns); - var columnsAndIndexes = JoinColumns(columns) + (indexes != null ? "," + indexes : string.Empty); - return columnsAndIndexes; - } - - protected virtual string JoinIndexes(IEnumerable columns) - { - var indexes = new List(); - foreach (var column in columns) - { - var indexSql = column.IndexSql; - - if (indexSql != null) - { - indexes.Add(indexSql); - } - } - - if (indexes.Count == 0) - { - return null; - } - - return string.Join(", ", [.. indexes]); - } - protected virtual string JoinColumns(IEnumerable columns) { var columnStrings = new List(); @@ -1932,15 +1738,15 @@ protected virtual void CreateSchemaInfoTable() if (!TableExists(_schemaInfotable)) { AddTable(_schemaInfotable, - new Column("Version", DbType.Int64, ColumnProperty.NotNull | ColumnProperty.PrimaryKey), - new Column("Scope", DbType.String, 50, ColumnProperty.NotNull | ColumnProperty.PrimaryKey, "default"), + new Column("Version",DbType.Int64){IsNullable = false}, + new Column("Scope",DbType.String,50,"default"){IsNullable = false}, new Column("TimeStamp", DbType.DateTime)); } else { if (!ColumnExists(_schemaInfotable, "Scope")) { - AddColumn(_schemaInfotable, "Scope", DbType.String, 50, ColumnProperty.NotNull, "default"); + AddColumn(_schemaInfotable, new Column("Scope", DbType.String, 50) { IsNullable = false, DefaultValue = "default" }); RemoveAllConstraints(_schemaInfotable); AddPrimaryKey("PK_SchemaInfo", _schemaInfotable, ["Version", "Scope"]); } From ebdbb42d7bfc99aa783f2b1778858945e559d1e2 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 19:58:23 +0200 Subject: [PATCH 22/34] Fix v13 provider catalog regressions and share SQLite table rendering Replace the generic INFORMATION_SCHEMA foreign-key lookup with parameterized native readers for SQL Server, PostgreSQL, MySQL/MariaDB, Db2 and Firebird. Add native Informix and ASE readers, preserving composite column pairing and keeping unique indexes separate from declared constraints in metadata. Render Informix inline constraint names using its suffix syntax. Treat Oracle DEFAULT NULL as absence of a value, including string columns. Make tests request unique constraints explicitly after ChangeColumn and remove unrelated unsigned attributes from PostgreSQL identity/index fixtures. Use the same pure SQLite CREATE TABLE renderer for imperative execution and structured preview, including named identity keys, uniqueness and collation. Verify generated SQL against SQLite rather than only asserting its text. Validation: solution build; 87 unit tests and 193 SQLite tests pass locally. The new composite-FK catalog regression runs in six live database matrix jobs. This follow-up addresses failures found in PR #181 run 35756650482; live database verification is required on the new head before the PR is ready. --- .../Providers/Live/LiveDatabaseTests.cs | 18 +++ .../Live/LiveMetadataRegressionTests.cs | 3 +- ...ionProvider_PrimaryKeyWithIdentityTests.cs | 2 +- ...ansformationProvider_ReservedWordsTests.cs | 2 +- ...SQLiteTransformationProvider_GetUniques.cs | 6 +- src/Migrator.Tests/SchemaConstraintTests.cs | 17 +++ src/Migrator/Framework/Fluent/Operations.cs | 10 +- .../Providers/ForeignKeyMetadataReader.cs | 93 +++++++++++++ .../Impl/Informix/InformixDialect.cs | 17 +++ .../InformixTransformationProvider.cs | 15 ++ .../Oracle/OracleTransformationProvider.cs | 2 +- .../Providers/Impl/SQLite/SQLiteTableSql.cs | 128 ++++++++++++++++++ .../SQLite/SQLiteTransformationProvider.cs | 111 +-------------- .../Sybase/SybaseTransformationProvider.cs | 25 ++++ .../Providers/TransformationProvider.cs | 74 +--------- 15 files changed, 334 insertions(+), 189 deletions(-) create mode 100644 src/Migrator/Providers/ForeignKeyMetadataReader.cs create mode 100644 src/Migrator/Providers/Impl/SQLite/SQLiteTableSql.cs diff --git a/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs b/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs index aba5077d..c9a99d6e 100644 --- a/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs +++ b/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs @@ -33,6 +33,24 @@ internal void RunRegression(Action action) finally { TearDown(); } } + [Test] + public void ConstraintMetadataPreservesForeignKeyPairsAndSeparatesUniqueIndexes() + { + provider.AddTable("parents", new Column("first_id", DbType.Int32), new Column("second_id", DbType.Int32), + new PrimaryKeyConstraint("pk_parents", "second_id", "first_id")); + provider.AddTable("children", new Column("left_id", DbType.Int32), new Column("right_id", DbType.Int32)); + provider.AddForeignKey("fk_pair", "children", new[] { "left_id", "right_id" }, "parents", new[] { "second_id", "first_id" }); + provider.AddIndex("children", new DbIndex { Name = "ux_separate", KeyColumns = new[] { "left_id" }, Unique = true }); + var constraints = provider.GetTableConstraints("children"); + var foreignKey = constraints.OfType().Single(); + Assert.That(foreignKey.ChildColumns.Select(c => c.ToLowerInvariant()), Is.EqualTo(new[] { "left_id", "right_id" })); + Assert.That(foreignKey.ParentColumns.Select(c => c.ToLowerInvariant()), Is.EqualTo(new[] { "second_id", "first_id" })); + Assert.That(constraints.OfType(), Is.Empty); + provider.Insert("parents", new[] { "first_id", "second_id" }, new object[] { 1, 2 }); + provider.Insert("children", new[] { "left_id", "right_id" }, new object[] { 2, 1 }); + AssertDatabaseError(() => provider.Insert("children", new[] { "left_id", "right_id" }, new object[] { 1, 2 })); + } + internal void DropCreatedDatabase() { provider.DropDatabases(provider.GetDatabases().Single()); diff --git a/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs b/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs index ebbdddf4..fb58c8eb 100644 --- a/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs +++ b/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs @@ -54,11 +54,12 @@ public class LiveMetadataRegressionTests [TestCase("Db2", ProviderTypes.IBM_DB2, Category = "Db2")] [TestCase("Firebird", ProviderTypes.Firebird, Category = "Firebird")] [TestCase("Sybase", ProviderTypes.Sybase, Category = "Sybase")] - public void ChangeColumnCreatesRequestedUniqueConstraint(string database, ProviderTypes type) => new LiveDatabaseTests(database, type).RunRegression(f => + public void ExplicitUniqueAfterChangeColumnEnforcesUniqueness(string database, ProviderTypes type) => new LiveDatabaseTests(database, type).RunRegression(f => { f.Provider.AddTable("unique_values", new Column("amount",DbType.Int32){IsNullable = false}); f.Provider.Insert("unique_values", ["amount"], [7]); f.Provider.ChangeColumn("unique_values", new Column("amount",DbType.Int64){IsNullable = false}); + f.Provider.AddUniqueConstraint("UX_unique_values_amount", "unique_values", "amount"); Assert.That(f.Provider.ConstraintExists("unique_values", "UX_unique_values_amount"), Is.True); Assert.That(f.Provider.GetIndexes("unique_values").Any(i => i.UniqueConstraint && i.KeyColumns.Single().Equals("amount", StringComparison.OrdinalIgnoreCase)), Is.True); f.AssertDatabaseError(() => f.Provider.Insert("unique_values", ["amount"], [7L])); diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs index 7a38a879..983593c0 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs @@ -20,7 +20,7 @@ public void AddTableWithPrimaryKeyIdentity_Succeeds() Provider.AddTable(testTableName, new Column(propertyName1,DbType.Int32){IsNullable = false,IsIdentity = true}, - new Column(propertyName2,DbType.Int32){IsUnsigned = true},new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); + new Column(propertyName2,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); // Act Provider.Insert(testTableName, [propertyName2], [1]); diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs index 8dd92329..9da95692 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ReservedWordsTests.cs @@ -19,7 +19,7 @@ public void AddIndex_IncludeColumnsWithReservedWord_Succeeds() Provider.AddTable(testTableName, new Column(propertyName1,DbType.Int32){IsNullable = false,IsIdentity = true}, - new Column(propertyName2,DbType.Int32){IsUnsigned = true},new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); + new Column(propertyName2,DbType.Int32),new PrimaryKeyConstraint("PK_" + testTableName, propertyName1) ); // Act/Assert Provider.AddIndex(testTableName, new Index diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs index ed72a989..3804e8b6 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs @@ -54,9 +54,9 @@ public void GetUniques_Success() Assert.That(uniqueConstraints.Single(x => x.Name == uniqueConstraintName1).KeyColumns, Is.EqualTo([property3])); Assert.That(uniqueConstraints.Single(x => x.Name == uniqueConstraintName2).KeyColumns, Is.EqualTo([property4, property5])); - 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 UniqueConstraint0 UNIQUE (Property2)")); + 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 \"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 index 34f1e472..345d2d1b 100644 --- a/src/Migrator.Tests/SchemaConstraintTests.cs +++ b/src/Migrator.Tests/SchemaConstraintTests.cs @@ -105,6 +105,23 @@ public void FluentNamedDefinitionsAreCompleteBeforeExecution() Assert.That(provider.GetTableConstraints("FluentKeys").Length, Is.EqualTo(2)); } + [Test] + public void OfflineIdentityPreviewExecutesTheSameSchemaAsImperativeCreation() + { + var builder = new MigrationBuilder(); + builder.Create.Table("PreviewIdentity").WithColumn("Id").AsInt32().Identity() + .WithColumn("Value").AsString(20).WithCollation("NOCASE") + .WithPrimaryKey("PK_PreviewIdentity", "Id") + .WithUniqueConstraint("UQ_Value", "Value"); + var sql = builder.Preview(new SqlGenerationContext(ProviderTypes.SQLite)).Single(); + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.ExecuteNonQuery(sql); + provider.ExecuteNonQuery("INSERT INTO PreviewIdentity (Value) VALUES ('Hello')"); + Assert.That(Convert.ToInt64(provider.ExecuteScalar("SELECT Id FROM PreviewIdentity")), Is.EqualTo(1)); + Assert.Catch(() => provider.ExecuteNonQuery("INSERT INTO PreviewIdentity (Value) VALUES ('HELLO')")); + Assert.That(provider.GetTableConstraints("PreviewIdentity").OfType().Single().Name, Is.EqualTo("PK_PreviewIdentity")); + } + [Test] public void InvalidKeyDefinitionsFailBeforeCreatingTheTable() { diff --git a/src/Migrator/Framework/Fluent/Operations.cs b/src/Migrator/Framework/Fluent/Operations.cs index 9ad7c22d..f5d1d67e 100644 --- a/src/Migrator/Framework/Fluent/Operations.cs +++ b/src/Migrator/Framework/Fluent/Operations.cs @@ -31,6 +31,14 @@ public override void Apply(ITransformationProvider p) public override MigrationOperation Reverse() => new RemoveOperation(RemoveKind.Table, Table); public override string ToSql(SqlGenerationContext c) { + if (c.Provider is ProviderTypes.SQLite or ProviderTypes.MonoSQLite) + { + if (Engine != null || Fields.Any(f => f is Index)) + throw new NotSupportedException("Preview table indexes as separate operations; SQLite table engines are unsupported."); + var sql = DotNetProjects.Migrator.Providers.Impl.SQLite.SQLiteTableSql.Generate(c.Dialect, c.Table(Table), Fields); + c.AddTable(Table, Fields.OfType()); + return sql + ";"; + } 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(); @@ -38,8 +46,6 @@ public override string ToSql(SqlGenerationContext c) { foreach (var column in columns.Where(x => primary.KeyColumns.Contains(x.Name))) column.IsNullable = false; - if (c.Provider == ProviderTypes.SQLite && columns.Any(x => x.IsIdentity)) - throw new NotSupportedException("Named SQLite identity-key preview requires the complete table generator."); } var definitions = columns.Select(c.Column).ToList(); definitions.AddRange(Fields.OfType().Select(c.Dialect.GetTableConstraintSql)); diff --git a/src/Migrator/Providers/ForeignKeyMetadataReader.cs b/src/Migrator/Providers/ForeignKeyMetadataReader.cs new file mode 100644 index 00000000..5929308d --- /dev/null +++ b/src/Migrator/Providers/ForeignKeyMetadataReader.cs @@ -0,0 +1,93 @@ +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.Mysql; +using DotNetProjects.Migrator.Providers.Impl.DB2; +using DotNetProjects.Migrator.Providers.Impl.Firebird; +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; + +namespace DotNetProjects.Migrator.Providers; + +internal static class ForeignKeyMetadataReader +{ + public static ForeignKeyConstraint[] Read(TransformationProvider provider, string table) + { + var parameterTable = table; + string schema = null; + string sql; + if (provider.Dialect is SqlServerDialect) + sql = @"SELECT f.name,OBJECT_NAME(f.referenced_object_id),cc.name,pc.name,k.constraint_column_id, + REPLACE(f.delete_referential_action_desc,'_',' '),REPLACE(f.update_referential_action_desc,'_',' ') + FROM sys.foreign_keys f JOIN sys.foreign_key_columns k ON k.constraint_object_id=f.object_id + JOIN sys.columns cc ON cc.object_id=k.parent_object_id AND cc.column_id=k.parent_column_id + JOIN sys.columns pc ON pc.object_id=k.referenced_object_id AND pc.column_id=k.referenced_column_id + WHERE f.parent_object_id=OBJECT_ID(@lookup_table) ORDER BY f.name,k.constraint_column_id"; + else if (provider.Dialect is PostgreSQLDialect) + { + parameterTable = provider.QuoteTableNameIfRequired(table); + sql = @"SELECT c.conname,p.relname,cc.attname,pc.attname,k.ordinality,c.confdeltype::text,c.confupdtype::text + FROM pg_constraint c JOIN pg_class p ON p.oid=c.confrelid + CROSS JOIN LATERAL unnest(c.conkey,c.confkey) WITH ORDINALITY k(childnum,parentnum,ordinality) + JOIN pg_attribute cc ON cc.attrelid=c.conrelid AND cc.attnum=k.childnum + JOIN pg_attribute pc ON pc.attrelid=c.confrelid AND pc.attnum=k.parentnum + WHERE c.conrelid=to_regclass(@lookup_table) AND c.contype='f' ORDER BY c.conname,k.ordinality"; + } + else if (provider.Dialect is MysqlDialect) + { + var parts = table.Split('.'); + if (parts.Length > 2 || parts.Any(p => p.Contains((char)96))) throw new NotSupportedException("Use unquoted names for MySQL foreign-key catalog lookup."); + parameterTable = parts[^1]; schema = parts.Length == 2 ? parts[0] : null; + sql = @"SELECT k.CONSTRAINT_NAME,k.REFERENCED_TABLE_NAME,k.COLUMN_NAME,k.REFERENCED_COLUMN_NAME,k.ORDINAL_POSITION,r.DELETE_RULE,r.UPDATE_RULE + FROM information_schema.KEY_COLUMN_USAGE k JOIN information_schema.REFERENTIAL_CONSTRAINTS r + ON r.CONSTRAINT_SCHEMA=k.CONSTRAINT_SCHEMA AND r.TABLE_NAME=k.TABLE_NAME AND r.CONSTRAINT_NAME=k.CONSTRAINT_NAME + WHERE k.TABLE_NAME=@lookup_table AND k.TABLE_SCHEMA=COALESCE(@lookup_schema,DATABASE()) + AND k.REFERENCED_TABLE_NAME IS NOT NULL ORDER BY k.CONSTRAINT_NAME,k.ORDINAL_POSITION"; + } + else if (provider.Dialect is DB2Dialect) + { + parameterTable = table.StartsWith('"') ? table.Trim('"') : table.ToUpperInvariant(); + sql = @"SELECT r.CONSTNAME,r.REFTABNAME,c.COLNAME,p.COLNAME,c.COLSEQ,r.DELETERULE,r.UPDATERULE + FROM SYSCAT.REFERENCES r JOIN SYSCAT.KEYCOLUSE c ON c.TABSCHEMA=r.TABSCHEMA AND c.TABNAME=r.TABNAME AND c.CONSTNAME=r.CONSTNAME + JOIN SYSCAT.KEYCOLUSE p ON p.TABSCHEMA=r.REFTABSCHEMA AND p.TABNAME=r.REFTABNAME AND p.CONSTNAME=r.REFKEYNAME AND p.COLSEQ=c.COLSEQ + WHERE r.TABSCHEMA=CURRENT SCHEMA AND r.TABNAME=@lookup_table ORDER BY r.CONSTNAME,c.COLSEQ"; + } + else if (provider.Dialect is FirebirdDialect) + { + parameterTable = table.StartsWith('"') ? table.Trim('"') : table.ToUpperInvariant(); + sql = @"SELECT TRIM(c.RDB$CONSTRAINT_NAME),TRIM(p.RDB$RELATION_NAME),TRIM(ck.RDB$FIELD_NAME),TRIM(pk.RDB$FIELD_NAME), + ck.RDB$FIELD_POSITION,TRIM(r.RDB$DELETE_RULE),TRIM(r.RDB$UPDATE_RULE) + FROM RDB$RELATION_CONSTRAINTS c JOIN RDB$REF_CONSTRAINTS r ON r.RDB$CONSTRAINT_NAME=c.RDB$CONSTRAINT_NAME + JOIN RDB$RELATION_CONSTRAINTS p ON p.RDB$CONSTRAINT_NAME=r.RDB$CONST_NAME_UQ + JOIN RDB$INDEX_SEGMENTS ck ON ck.RDB$INDEX_NAME=c.RDB$INDEX_NAME + JOIN RDB$INDEX_SEGMENTS pk ON pk.RDB$INDEX_NAME=p.RDB$INDEX_NAME AND pk.RDB$FIELD_POSITION=ck.RDB$FIELD_POSITION + WHERE c.RDB$RELATION_NAME=@lookup_table ORDER BY c.RDB$CONSTRAINT_NAME,ck.RDB$FIELD_POSITION"; + } + else throw new NotSupportedException("Foreign-key metadata is unsupported by " + provider.Dialect.GetType().Name + "."); + using var command = provider.CreateCommand(); + AddParameter(command, "lookup_table", parameterTable); + if (provider.Dialect is MysqlDialect) AddParameter(command, "lookup_schema", schema); + var rows = new List<(string Name, string Parent, string ChildColumn, string ParentColumn, string Delete, string Update)>(); + using (var reader = provider.ExecuteQuery(command, sql)) + while (reader.Read()) + rows.Add((reader.GetString(0).Trim(), reader.GetString(1).Trim(), reader.GetString(2).Trim(), reader.GetString(3).Trim(), + Action(reader.GetString(5)), Action(reader.GetString(6)))); + return rows.GroupBy(r => r.Name).Select(group => new ForeignKeyConstraint(group.Key, group.First().Parent, + group.Select(r => r.ParentColumn).ToArray(), table, group.Select(r => r.ChildColumn).ToArray()) + { OnDelete = group.First().Delete, OnUpdate = group.First().Update }).ToArray(); + } + private static string Action(string value) => value.Trim().ToUpperInvariant() switch + { + "A" => "NO ACTION", "R" => "RESTRICT", "C" => "CASCADE", "N" => "SET NULL", "D" => "SET DEFAULT", + "NO ACTION" or "RESTRICT" or "CASCADE" or "SET NULL" or "SET DEFAULT" => value.Trim().ToUpperInvariant(), + _ => throw new MigrationException("Unknown foreign-key action in catalog: " + value) + }; + 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/Impl/Informix/InformixDialect.cs b/src/Migrator/Providers/Impl/Informix/InformixDialect.cs index 2e3befc7..0b703122 100644 --- a/src/Migrator/Providers/Impl/Informix/InformixDialect.cs +++ b/src/Migrator/Providers/Impl/Informix/InformixDialect.cs @@ -1,3 +1,4 @@ +using System; using System.Data; using DotNetProjects.Migrator.Framework; @@ -42,6 +43,22 @@ public InformixDialect() RegisterColumnAttribute(ColumnAttribute.Identity, ""); } + public override string GetTableConstraintSql(TableConstraint constraint) + { + var copy = constraint switch + { + PrimaryKeyConstraint p => (TableConstraint)new PrimaryKeyConstraint(null, p.KeyColumns) { NonClustered = p.NonClustered }, + DotNetProjects.Migrator.Framework.UniqueConstraint u => new DotNetProjects.Migrator.Framework.UniqueConstraint(null, u.KeyColumns), + CheckConstraint c => new CheckConstraint(null, c.CheckConstraintString), + _ => throw new NotSupportedException("Unsupported Informix table constraint.") + }; + var body = base.GetTableConstraintSql(copy); + if (constraint.Name == null) return body; + if (!System.Text.RegularExpressions.Regex.IsMatch(constraint.Name, @"^[A-Za-z_][A-Za-z0-9_$]*$")) + throw new NotSupportedException("Informix constraint names require simple identifiers unless DELIMIDENT is configured."); + return body + " CONSTRAINT " + constraint.Name; + } + public override string Default(object value) => value is bool boolean ? (boolean ? "DEFAULT 't'" : "DEFAULT 'f'") : base.Default(value); public override ColumnPropertiesMapper GetColumnMapper(Column column) diff --git a/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs b/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs index 199da853..e9174cfb 100644 --- a/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs @@ -1,3 +1,4 @@ +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; using System; using System.Collections.Generic; using System.Data; @@ -95,6 +96,20 @@ public override Column[] GetColumns(string table) return columns.ToArray(); } + public override ForeignKeyConstraint[] GetForeignKeyConstraints(string table) + { + var rows = new List<(string Name, string Parent, string ChildIndex, string ParentIndex, string Delete)>(); + using (var command = CreateCommand()) + using (var reader = ExecuteQuery(command, $"SELECT c.constrname,t2.tabname,c.idxname,p.idxname,r.delrule FROM sysconstraints c JOIN systables t ON t.tabid=c.tabid JOIN sysreferences r ON r.constrid=c.constrid JOIN sysconstraints p ON p.constrid=r.primary JOIN systables t2 ON t2.tabid=r.ptabid WHERE t.owner=USER AND t.tabname='{Name(table)}' AND t2.owner=USER ORDER BY c.constrname")) + while (reader.Read()) + rows.Add((reader.GetString(0).Trim(), reader.GetString(1).Trim(), reader.GetString(2).Trim(), reader.GetString(3).Trim(), reader.GetString(4).Trim())); + var childIndexes = GetIndexes(table).ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); + return rows.Select(row => new ForeignKeyConstraint(row.Name, row.Parent, + GetIndexes(row.Parent).Single(i => i.Name.Equals(row.ParentIndex, StringComparison.OrdinalIgnoreCase)).KeyColumns, + table, childIndexes[row.ChildIndex].KeyColumns) + { OnDelete = row.Delete == "C" ? "CASCADE" : "RESTRICT", OnUpdate = "RESTRICT" }).ToArray(); + } + public override TableConstraint[] GetTableConstraints(string table) { var indexes = GetIndexes(table).ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index 2a8ae3b9..e79fa2e7 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -599,7 +599,7 @@ public override Column[] GetColumns(string table) // dataDefaultString contains ISEQ$$ if the column is an identity column if ( !string.IsNullOrWhiteSpace(dataDefaultString) && - (column.Type == DbType.String || !dataDefaultString.Equals("null", StringComparison.OrdinalIgnoreCase)) && + !dataDefaultString.Trim().Equals("null", StringComparison.OrdinalIgnoreCase) && !dataDefaultString.Contains("ISEQ$$") && !dataDefaultString.Contains(".nextval")) { diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTableSql.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTableSql.cs new file mode 100644 index 00000000..e6022fdb --- /dev/null +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTableSql.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using DotNetProjects.Migrator.Framework; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace DotNetProjects.Migrator.Providers.Impl.SQLite; + +/// Pure SQLite table rendering shared by execution and offline preview. +internal static class SQLiteTableSql +{ + public static string Generate(Dialect dialect, string quotedTable, IDbField[] fields) + { + if (fields.Any(f => f is not (Column or PrimaryKeyConstraint or UniqueConstraint or CheckConstraint or ForeignKeyConstraint or Index))) + throw new NotSupportedException("Unsupported SQLite table definition."); + if (!fields.OfType().Any()) throw new MigrationException("A table requires columns."); + var columns = fields.Where(x => x is Column) + .Cast() + .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) + { + TransformationProvider.ValidateKeyColumns(explicitKey.Name, explicitKey.KeyColumns, columns); + if (explicitKey.NonClustered) throw new NotSupportedException("SQLite does not support nonclustered primary keys."); + foreach (var column in columns.Where(c => explicitKey.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) + column.IsNullable = false; + var identities = columns.Where(c => c.IsIdentity).ToArray(); + if (identities.Length != 0 && (identities.Length != 1 || explicitKey.KeyColumns.Length != 1 || !explicitKey.KeyColumns[0].Equals(identities[0].Name, StringComparison.OrdinalIgnoreCase) || dialect.GetTypeName(identities[0].Type) != "INTEGER")) + throw new MigrationException("SQLite identity requires one INTEGER primary-key column."); + } + foreach (var unique in fields.OfType()) TransformationProvider.ValidateKeyColumns(unique.Name, unique.KeyColumns, columns); + + if (explicitKey == null && columns.Any(c => c.IsIdentity)) + throw new MigrationException("SQLite identity requires an explicit INTEGER primary-key constraint."); + var columnSql = columns.Select(column => + { + var mapped = column.CopyDefinition(); + mapped.IsIdentity = false; + var sql = dialect.GetAndMapColumnProperties(mapped).ColumnSql; + if (column.IsIdentity) + sql += (explicitKey.Name == null ? "" : $" CONSTRAINT {dialect.QuoteIdentifier(explicitKey.Name)}") + " PRIMARY KEY AUTOINCREMENT"; + return sql; + }).ToList(); + if (explicitKey != null && !columns.Any(c => c.IsIdentity)) + columnSql.Add(dialect.GetTableConstraintSql(explicitKey)); + var table = quotedTable; + var stringBuilder = new StringBuilder($"CREATE TABLE {table} ({string.Join(", ", columnSql)}"); + + // Uniques + var uniques = fields.Where(x => x is UniqueConstraint).Cast().ToArray(); + + foreach (var u in uniques) + { + if (!string.IsNullOrEmpty(u.Name)) + { + stringBuilder.Append($", CONSTRAINT {dialect.QuoteIdentifier(u.Name)}"); + } + else + { + stringBuilder.Append(", "); + } + + var uniqueColumnsCommaSeparated = string.Join(", ", u.KeyColumns.Select(dialect.QuoteColumnNameIfRequired)); + stringBuilder.Append($" UNIQUE ({uniqueColumnsCommaSeparated})"); + } + + // Foreign keys + var foreignKeys = fields.Where(x => x is ForeignKeyConstraint).Cast().ToArray(); + + List foreignKeyStrings = []; + + foreach (var fk in foreignKeys) + { + var sourceColumnNamesQuotedString = string.Join(", ", fk.ChildColumns.Select(dialect.QuoteColumnNameIfRequired)); + var parentColumnNamesQuotedString = string.Join(", ", fk.ParentColumns.Select(dialect.QuoteColumnNameIfRequired)); + var parentTableNameQuoted = dialect.QuoteTableNameIfRequired(fk.ParentTable); + + var foreignKeySql = (fk.Name == null ? "" : $"CONSTRAINT {dialect.QuoteIdentifier(fk.Name)} ") + + $"FOREIGN KEY ({sourceColumnNamesQuotedString}) REFERENCES {parentTableNameQuoted}" + + (fk.ParentColumns.Length == 0 ? "" : $"({parentColumnNamesQuotedString})"); + if (!string.IsNullOrWhiteSpace(fk.OnDelete) && !string.Equals(fk.OnDelete, "NO ACTION", StringComparison.OrdinalIgnoreCase)) + { + foreignKeySql += $" ON DELETE {ValidateAction(fk.OnDelete)}"; + } + + if (!string.IsNullOrWhiteSpace(fk.OnUpdate)) foreignKeySql += $" ON UPDATE {ValidateAction(fk.OnUpdate)}"; + foreignKeyStrings.Add(foreignKeySql); + } + + if (foreignKeyStrings.Count > 0) + { + stringBuilder.Append(", "); + stringBuilder.Append(string.Join(", ", foreignKeyStrings)); + } + + // Check Constraints + var checkConstraints = fields.Where(x => x is CheckConstraint).OfType().ToArray(); + List checkConstraintStrings = []; + + foreach (var checkConstraint in checkConstraints) + { + checkConstraintStrings.Add(dialect.GetTableConstraintSql(checkConstraint)); + } + + if (checkConstraintStrings.Count > 0) + { + stringBuilder.Append($", {string.Join(", ", checkConstraintStrings)}"); + } + + stringBuilder.Append(')'); + + return stringBuilder.ToString(); + } + + private static string ValidateAction(string action) + { + var value = action.ToUpperInvariant(); + if (value is not ("CASCADE" or "RESTRICT" or "SET NULL" or "SET DEFAULT" or "NO ACTION")) + throw new MigrationException("Unsupported foreign-key action: " + action); + return value; + } +} diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index cf0a9553..02785fee 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1353,115 +1353,10 @@ public override Index[] GetIndexes(string table) public override void AddTable(string name, string engine, params IDbField[] fields) { - var columns = fields.Where(x => x is Column) - .Cast() - .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."); - foreach (var column in columns.Where(c => explicitKey.KeyColumns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))) - column.IsNullable = false; - var identities = columns.Where(c => c.IsIdentity).ToArray(); - if (identities.Length != 0 && (identities.Length != 1 || explicitKey.KeyColumns.Length != 1 || !explicitKey.KeyColumns[0].Equals(identities[0].Name, StringComparison.OrdinalIgnoreCase) || _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); - - if (explicitKey == null && columns.Any(c => c.IsIdentity)) - throw new MigrationException("SQLite identity requires an explicit INTEGER primary-key constraint."); - var columnSql = columns.Select(column => - { - var mapped = column.CopyDefinition(); - mapped.IsIdentity = false; - var sql = _dialect.GetAndMapColumnProperties(mapped).ColumnSql; - if (column.IsIdentity) - sql += (explicitKey.Name == null ? "" : $" CONSTRAINT {_dialect.QuoteIdentifier(explicitKey.Name)}") + " PRIMARY KEY AUTOINCREMENT"; - return sql; - }).ToList(); - if (explicitKey != null && !columns.Any(c => c.IsIdentity)) - columnSql.Add(_dialect.GetTableConstraintSql(explicitKey)); + if (engine != null) throw new NotSupportedException("SQLite does not support table engines."); var table = _dialect.TableNameNeedsQuote ? _dialect.Quote(name) : QuoteTableNameIfRequired(name); - var stringBuilder = new StringBuilder($"CREATE TABLE {table} ({string.Join(", ", columnSql)}"); - - // Uniques - var uniques = fields.Where(x => x is UniqueConstraint).Cast().ToArray(); - - foreach (var u in uniques) - { - if (!string.IsNullOrEmpty(u.Name)) - { - stringBuilder.Append($", CONSTRAINT {QuoteConstraintNameIfRequired(u.Name)}"); - } - else - { - stringBuilder.Append(", "); - } - - var uniqueColumnsCommaSeparated = string.Join(", ", u.KeyColumns.Select(QuoteColumnNameIfRequired)); - stringBuilder.Append($" UNIQUE ({uniqueColumnsCommaSeparated})"); - } - - // Foreign keys - var foreignKeys = fields.Where(x => x is ForeignKeyConstraint).Cast().ToArray(); - - List foreignKeyStrings = []; - - foreach (var fk in foreignKeys) - { - var sourceColumnNamesQuotedString = string.Join(", ", fk.ChildColumns.Select(QuoteColumnNameIfRequired)); - var parentColumnNamesQuotedString = string.Join(", ", fk.ParentColumns.Select(QuoteColumnNameIfRequired)); - var parentTableNameQuoted = QuoteTableNameIfRequired(fk.ParentTable); - - var foreignKeySql = (fk.Name == null ? "" : $"CONSTRAINT {_dialect.QuoteIdentifier(fk.Name)} ") + - $"FOREIGN KEY ({sourceColumnNamesQuotedString}) REFERENCES {parentTableNameQuoted}" + - (fk.ParentColumns.Length == 0 ? "" : $"({parentColumnNamesQuotedString})"); - if (!string.IsNullOrWhiteSpace(fk.OnDelete) && !string.Equals(fk.OnDelete, "NO ACTION", StringComparison.OrdinalIgnoreCase)) - { - foreignKeySql += $" ON DELETE {ValidateForeignKeyAction(fk.OnDelete)}"; - } - - if (!string.IsNullOrWhiteSpace(fk.OnUpdate)) foreignKeySql += $" ON UPDATE {ValidateForeignKeyAction(fk.OnUpdate)}"; - foreignKeyStrings.Add(foreignKeySql); - } - - if (foreignKeyStrings.Count > 0) - { - stringBuilder.Append(", "); - stringBuilder.Append(string.Join(", ", foreignKeyStrings)); - } - - // Check Constraints - var checkConstraints = fields.Where(x => x is CheckConstraint).OfType().ToArray(); - List checkConstraintStrings = []; - - foreach (var checkConstraint in checkConstraints) - { - checkConstraintStrings.Add(_dialect.GetTableConstraintSql(checkConstraint)); - } - - if (checkConstraintStrings.Count > 0) - { - stringBuilder.Append($", {string.Join(", ", checkConstraintStrings)}"); - } - - stringBuilder.Append(')'); - - ExecuteNonQuery(stringBuilder.ToString()); - - var indexes = fields.Where(x => x is Index) - .Cast() - .ToArray(); - - foreach (var index in indexes) - { - AddIndex(name, index); - } + ExecuteNonQuery(SQLiteTableSql.Generate(_dialect, table, fields)); + foreach (var index in fields.OfType()) AddIndex(name, index); } public override string AddIndex(string table, Index index) diff --git a/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs b/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs index 96e9b7a4..30f94098 100644 --- a/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Sybase/SybaseTransformationProvider.cs @@ -1,3 +1,4 @@ +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; using System; using System.Collections.Generic; using System.Data; @@ -80,6 +81,30 @@ public override Column[] GetColumns(string table) return columns.ToArray(); } + public override ForeignKeyConstraint[] GetForeignKeyConstraints(string table) + { + var columns = string.Join(",", Enumerable.Range(1, 16).Select(n => $"col_name(r.tableid,r.fokey{n}),col_name(r.reftabid,r.refkey{n})")); + var result = new List(); + using var command = CreateCommand(); + using var reader = ExecuteQuery(command, $"SELECT object_name(r.constrid),object_name(r.reftabid),r.keycnt,r.frgndbname,r.pmrydbname,{columns} FROM sysreferences r WHERE r.tableid=object_id('{Literal(table)}') ORDER BY r.constrid"); + while (reader.Read()) + { + if (!reader.IsDBNull(3) || !reader.IsDBNull(4)) + throw new NotSupportedException("Cross-database ASE foreign keys require qualified metadata support."); + var count = Convert.ToInt32(reader.GetValue(2)); + if (count is < 1 or > 16) throw new NotSupportedException("Unsupported ASE foreign-key column count."); + var children = new string[count]; var parents = new string[count]; + for (var index = 0; index < count; index++) + { + children[index] = reader.GetString(5 + index * 2); + parents[index] = reader.GetString(6 + index * 2); + } + result.Add(new ForeignKeyConstraint(reader.GetString(0), reader.GetString(1), parents, table, children) + { OnDelete = "NO ACTION", OnUpdate = "NO ACTION" }); + } + return result.ToArray(); + } + public override TableConstraint[] GetTableConstraints(string table) { var constraints = new List(); diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index db1603a2..bf8a43e6 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -148,77 +148,7 @@ public virtual Column[] GetColumns(string table) /// /// /// - public virtual ForeignKeyConstraint[] GetForeignKeyConstraints(string table) - { - var constraints = new List(); - var sb = new StringBuilder(); - sb.AppendLine("SELECT"); - sb.AppendLine(" tc.CONSTRAINT_NAME AS FK_KEY,"); - sb.AppendLine(" tc.TABLE_SCHEMA,"); - sb.AppendLine(" tc.TABLE_NAME AS CHILD_TABLE,"); - sb.AppendLine(" kcu.COLUMN_NAME AS CHILD_COLUMN,"); - sb.AppendLine(" ccu.TABLE_NAME AS PARENT_TABLE,"); - sb.AppendLine(" ccu.COLUMN_NAME AS PARENT_COLUMN"); - sb.AppendLine("FROM "); - sb.AppendLine(" INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc "); - sb.AppendLine("JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE as kcu"); - sb.AppendLine(" ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA"); - sb.AppendLine("JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS as rc"); - sb.AppendLine(" ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA"); - sb.AppendLine("JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu"); - sb.AppendLine(" ON rc.UNIQUE_CONSTRAINT_NAME = ccu.CONSTRAINT_NAME AND rc.UNIQUE_CONSTRAINT_SCHEMA = ccu.CONSTRAINT_SCHEMA"); - sb.AppendLine($"WHERE LOWER(tc.TABLE_NAME) = LOWER('{table}') AND tc.CONSTRAINT_TYPE = 'FOREIGN KEY'"); - sb.AppendLine("ORDER BY kcu.ORDINAL_POSITION"); - - var sql = sb.ToString(); - List foreignKeyConstraintItems = []; - - using (var cmd = CreateCommand()) - using (var reader = ExecuteQuery(cmd, sql)) - { - while (reader.Read()) - { - var constraintItem = new ForeignKeyConstraintItem - { - SchemaName = reader.GetString(reader.GetOrdinal("TABLE_SCHEMA")), - ForeignKeyName = reader.GetString(reader.GetOrdinal("FK_KEY")), - ChildTableName = reader.GetString(reader.GetOrdinal("CHILD_TABLE")), - ChildColumnName = reader.GetString(reader.GetOrdinal("CHILD_COLUMN")), - ParentTableName = reader.GetString(reader.GetOrdinal("PARENT_TABLE")), - ParentColumnName = reader.GetString(reader.GetOrdinal("PARENT_COLUMN")) - }; - - foreignKeyConstraintItems.Add(constraintItem); - } - } - - var schemaChildTableGroups = foreignKeyConstraintItems.GroupBy(x => new { x.SchemaName, x.ChildTableName }).Count(); - - if (schemaChildTableGroups > 1) - { - throw new MigrationException($"Duplicates found (grouping by schema name and child table name). Since we do not offer schemas in '{nameof(GetForeignKeyConstraints)}' at this moment in time we cannot filter your target schema. Your database use the same table name in different schemas."); - } - - var groups = foreignKeyConstraintItems.GroupBy(x => x.ForeignKeyName); - - foreach (var group in groups) - { - var first = group.First(); - - var foreignKeyConstraint = new ForeignKeyConstraint - { - Name = first.ForeignKeyName, - ParentTable = first.ParentTableName, - ParentColumns = [.. group.Select(x => x.ParentColumnName).Distinct()], - ChildTable = first.ChildTableName, - ChildColumns = [.. group.Select(x => x.ChildColumnName).Distinct()] - }; - - constraints.Add(foreignKeyConstraint); - } - - return [.. constraints]; - } + public virtual ForeignKeyConstraint[] GetForeignKeyConstraints(string table) => ForeignKeyMetadataReader.Read(this, table); public virtual TableConstraint[] GetTableConstraints(string table) => ConstraintMetadataReader.Read(this, table); @@ -419,7 +349,7 @@ public virtual void AddTable(string name, string engine, params IDbField[] field foreach (var foreignKey in fields.OfType()) AddForeignKey(name, foreignKey); } - protected static void ValidateKeyColumns(string name, string[] keys, Column[] columns) + protected internal static void ValidateKeyColumns(string name, string[] keys, Column[] columns) { if (name != null && string.IsNullOrWhiteSpace(name)) throw new MigrationException("A constraint name must not be empty."); if (keys == null || keys.Length == 0 || keys.Any(string.IsNullOrWhiteSpace) || keys.Distinct(StringComparer.OrdinalIgnoreCase).Count() != keys.Length) From 529e35ea7ee27c5ad7e963f74e9319da8d02f040 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:04:51 +0200 Subject: [PATCH 23/34] Add explicit SQL defaults and semantic column collations for both authoring APIs Introduce RawSql.Insert for trusted default expressions without changing literal string escaping. SQLite renders the required expression parentheses and preserves typed expressions, escaped literals, GUIDs and numeric defaults through catalog reads and table reconstruction. Replace the new string-only collation property with a typed Collation definition. Both imperative columns and fluent builders accept the same semantic presets or named provider escape hatch. Resolve supported comparisons in SQL Server, MySQL, MariaDB, PostgreSQL and SQLite dialects; reject unmapped requests and non-text columns before execution. Never substitute SQLite ASCII folding for Unicode case-insensitivity. Document expression trust, function availability, provider/version requirements, language and ordering limits, PostgreSQL ICU configuration and metadata changes in the 12.1-to-13 migration guide. Account for MySQL/MariaDB catalog equivalence between unique indexes and unique constraints in the composite-FK regression. Validation: solution build; 87 unit and 195 SQLite tests pass locally. Added live case/accent comparison tests for MySQL/MariaDB and SQL Server, plus SQL Server NEWID defaults in imperative and fluent creation. Live jobs must pass on this head. --- docs/migration-guide-12.1-to-13.md | 53 +++++++++++++++ .../Providers/Live/LiveDatabaseTests.cs | 5 +- .../Live/LiveMetadataRegressionTests.cs | 19 ++++++ ...erverTransformationProviderGenericTests.cs | 19 ++++++ src/Migrator.Tests/SchemaConstraintTests.cs | 51 +++++++++++++++ src/Migrator/Framework/Collation.cs | 28 ++++++++ src/Migrator/Framework/Column.cs | 2 +- .../Framework/Fluent/MigrationBuilder.cs | 4 +- src/Migrator/Framework/IColumn.cs | 2 +- src/Migrator/Framework/IDialect.cs | 1 + src/Migrator/Framework/RawSql.cs | 19 ++++++ src/Migrator/Providers/CatalogDefaultValue.cs | 13 ++-- .../Providers/ColumnPropertiesMapper.cs | 6 +- src/Migrator/Providers/Dialect.cs | 11 ++++ .../Providers/Impl/Mysql/MariaDBDialect.cs | 6 ++ .../Providers/Impl/Mysql/MysqlDialect.cs | 6 ++ .../Impl/PostgreSQL/PostgreSQLDialect.cs | 6 ++ .../Providers/Impl/SQLite/SQLiteDialect.cs | 7 ++ .../SQLite/SQLiteTransformationProvider.cs | 64 +------------------ .../Impl/SqlServer/SqlServerDialect.cs | 6 ++ 20 files changed, 254 insertions(+), 74 deletions(-) create mode 100644 src/Migrator/Framework/Collation.cs create mode 100644 src/Migrator/Framework/RawSql.cs diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index caf8e090..39d89783 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -143,3 +143,56 @@ 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. + +## Explicit SQL defaults and semantic collations + +`RawSql.Insert("ksuid_new()")` marks trusted SQL as an expression in either API: + +```csharp +new Column("Id", DbType.String, 27) { DefaultValue = RawSql.Insert("ksuid_new()") }; +// Fluent: +builder.Create.Table("Events").WithColumn("Id").AsString(27) + .WithDefaultValue(RawSql.Insert("ksuid_new()")); +``` + +The database must provide that function. Strings remain quoted values, so +`WithDefaultValue("ksuid_new()")` stores that text instead of calling a function. +SQLite wraps expressions in parentheses as required for expression defaults. +Metadata exposes unparsed SQL defaults as `RawSql`, replacing the previous private +expression object; inspect `RawSql.Sql` instead of assuming every default is a string. +Expression text is trusted migration code, not a parameter or a cross-database function abstraction. + +`Column.Collation` is now a typed `Collation` value. String assignment still selects +a provider name through an implicit conversion; `Collation.Named("name")` is explicit. +Fluent `.WithCollation(...)` takes the same type. + +```csharp +new Column("Name", DbType.String, 100) { Collation = Collation.CaseInsensitive }; +builder.Create.Table("Names").WithColumn("Name").AsString(100) + .WithCollation(Collation.CaseInsensitive); +``` + +| Preset | SQL Server | MySQL 8 | MariaDB 10.10+ | PostgreSQL | SQLite | +| --- | --- | --- | --- | --- | --- | +| `CaseInsensitive` (accent-sensitive) | Latin1 General 100 CI AS SC | utf8mb4 0900 as ci | utf8mb4 UCA1400 nopad as ci | Explicit installed name required | Unsupported | +| `CaseSensitive` (accent-sensitive) | Latin1 General 100 CS AS SC | utf8mb4 0900 as cs | utf8mb4 UCA1400 nopad as cs | Explicit installed name required | Explicit installed name required | +| `Binary` | Latin1 General 100 BIN2 | utf8mb4 0900 bin | utf8mb4 nopad bin | C | BINARY | +| `AsciiIgnoreCase` | Unsupported | Unsupported | Unsupported | Unsupported | NOCASE | + +These presets describe comparison intent, not identical sorting, normalization, +language tailoring, or trailing-space behavior across engines. Use a named collation +for a specific language or exact provider semantics. MySQL/MariaDB presets require +utf8mb4-compatible text columns and the listed engine versions. Other dialects reject +unmapped presets; custom dialects can override `ResolveCollation(CollationKind)`. +Unsupported requests fail during SQL generation, before executing the table operation. +SQLite never downgrades Unicode case-insensitivity to its ASCII-only NOCASE behavior. +SQLite rebuilds involving collated columns still fail before replacing the table. + +For PostgreSQL, create an ICU nondeterministic collation explicitly (for example +`CREATE COLLATION app_ci (provider=icu, locale='und-u-ks-level2', deterministic=false)`) +and use `Collation.Named("app_ci")`. The framework does not silently create shared +database objects while rendering a column or preview. + +MySQL/MariaDB expose unique indexes as unique constraints in their catalogs, so +metadata cannot recover whether the original author used CREATE UNIQUE INDEX or +a UNIQUE table clause. No ownership decision may be inferred from that syntax. diff --git a/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs b/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs index c9a99d6e..fe5f6db4 100644 --- a/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs +++ b/src/Migrator.Tests/Providers/Live/LiveDatabaseTests.cs @@ -45,7 +45,10 @@ public void ConstraintMetadataPreservesForeignKeyPairsAndSeparatesUniqueIndexes( var foreignKey = constraints.OfType().Single(); Assert.That(foreignKey.ChildColumns.Select(c => c.ToLowerInvariant()), Is.EqualTo(new[] { "left_id", "right_id" })); Assert.That(foreignKey.ParentColumns.Select(c => c.ToLowerInvariant()), Is.EqualTo(new[] { "second_id", "first_id" })); - Assert.That(constraints.OfType(), Is.Empty); + if (providerType is ProviderTypes.Mysql or ProviderTypes.MariaDB) + Assert.That(constraints.OfType().Single().Name, Is.EqualTo("ux_separate")); + else + Assert.That(constraints.OfType(), Is.Empty); provider.Insert("parents", new[] { "first_id", "second_id" }, new object[] { 1, 2 }); provider.Insert("children", new[] { "left_id", "right_id" }, new object[] { 2, 1 }); AssertDatabaseError(() => provider.Insert("children", new[] { "left_id", "right_id" }, new object[] { 1, 2 })); diff --git a/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs b/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs index fb58c8eb..640e43e9 100644 --- a/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs +++ b/src/Migrator.Tests/Providers/Live/LiveMetadataRegressionTests.cs @@ -10,6 +10,25 @@ namespace Migrator.Tests.Providers.Live; [NonParallelizable] public class LiveMetadataRegressionTests { + [TestCase("MySQL", ProviderTypes.Mysql, Category = "MySQL")] + [TestCase("MariaDB", ProviderTypes.MariaDB, Category = "MariaDB")] + public void SemanticCollationEnforcesCaseAndAccentSensitivity(string database, ProviderTypes type) => new LiveDatabaseTests(database, type).RunRegression(f => + { + var builder = new DotNetProjects.Migrator.Framework.Fluent.MigrationBuilder(); + builder.Create.Table("ci_names").WithColumn("name").AsString(40).WithCollation(Collation.CaseInsensitive) + .WithUniqueConstraint("uq_ci", "name"); + builder.Apply(f.Provider); + f.Provider.Insert("ci_names", ["name"], ["é"]); + f.AssertDatabaseError(() => f.Provider.Insert("ci_names", ["name"], ["É"])); + f.Provider.Insert("ci_names", ["name"], ["e"]); + f.Provider.AddTable("cs_names", new Column("name", DbType.String, 40) { Collation = Collation.CaseSensitive }, + new DotNetProjects.Migrator.Framework.UniqueConstraint("uq_cs", "name")); + f.Provider.Insert("cs_names", ["name"], ["é"]); + f.Provider.Insert("cs_names", ["name"], ["É"]); + Assert.That(Convert.ToInt32(f.Provider.ExecuteScalar("SELECT COUNT(*) FROM ci_names")), Is.EqualTo(2)); + Assert.That(Convert.ToInt32(f.Provider.ExecuteScalar("SELECT COUNT(*) FROM cs_names")), Is.EqualTo(2)); + }); + [Test, Category("Sybase")] public void SybaseLargeTextMetadataPreservesCapacity() => new LiveDatabaseTests("Sybase", ProviderTypes.Sybase).RunRegression(f => { diff --git a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs index eece559e..a33d2f98 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs @@ -1,4 +1,5 @@ using System.Data; +using DotNetProjects.Migrator.Framework; using System.Threading.Tasks; using DotNetProjects.Migrator.Providers; using DotNetProjects.Migrator.Providers.Impl.SqlServer; @@ -19,6 +20,24 @@ public async Task SetUpAsync() AddDefaultTable(); } + [Test] + public void SemanticCollationAndRawDefaultsExecuteInBothApis() + { + Provider.AddTable("SemanticNames", new Column("Name", DbType.String, 40) { Collation = Collation.CaseInsensitive }, + new Column("Token", DbType.Guid) { DefaultValue = RawSql.Insert("NEWID()") }); + Provider.Insert("SemanticNames", ["Name"], ["é"]); + Assert.That(System.Convert.ToInt32(Provider.ExecuteScalar("SELECT COUNT(*) FROM SemanticNames WHERE Name=N'É'")), Is.EqualTo(1)); + Assert.That(System.Convert.ToInt32(Provider.ExecuteScalar("SELECT COUNT(*) FROM SemanticNames WHERE Name=N'e'")), Is.EqualTo(0)); + Assert.That(Provider.ExecuteScalar("SELECT Token FROM SemanticNames"), Is.TypeOf()); + var builder = new DotNetProjects.Migrator.Framework.Fluent.MigrationBuilder(); + builder.Create.Table("SemanticNamesFluent").WithColumn("Name").AsString(40).WithCollation(Collation.CaseSensitive) + .WithColumn("Token").AsGuid().WithDefaultValue(RawSql.Insert("NEWID()")); + builder.Apply(Provider); + Provider.Insert("SemanticNamesFluent", ["Name"], ["é"]); + Assert.That(System.Convert.ToInt32(Provider.ExecuteScalar("SELECT COUNT(*) FROM SemanticNamesFluent WHERE Name=N'É'")), Is.EqualTo(0)); + Assert.That(Provider.ExecuteScalar("SELECT Token FROM SemanticNamesFluent"), Is.TypeOf()); + } + [Test] public void ByteColumnWillBeCreatedAsBlob() { diff --git a/src/Migrator.Tests/SchemaConstraintTests.cs b/src/Migrator.Tests/SchemaConstraintTests.cs index 345d2d1b..ac63d5e0 100644 --- a/src/Migrator.Tests/SchemaConstraintTests.cs +++ b/src/Migrator.Tests/SchemaConstraintTests.cs @@ -122,6 +122,57 @@ public void OfflineIdentityPreviewExecutesTheSameSchemaAsImperativeCreation() Assert.That(provider.GetTableConstraints("PreviewIdentity").OfType().Single().Name, Is.EqualTo("PK_PreviewIdentity")); } + [Test] + public void RawDefaultsWorkInBothApisAndSurviveMetadataAndRebuild() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.AddTable("RawImperative", + new Column("Id", DbType.Int32), + new Column("Token", DbType.String, 40) { DefaultValue = RawSql.Insert("lower(hex(randomblob(8)))") }, + new Column("Literal", DbType.String, 40) { DefaultValue = "lower(hex(randomblob(8)))" }); + var builder = new MigrationBuilder(); + builder.Create.Table("RawFluent").WithColumn("Id").AsInt32() + .WithColumn("Token").AsString(40).WithDefaultValue(RawSql.Insert("lower(hex(randomblob(8)))")) + .WithColumn("Literal").AsString(40).WithDefaultValue("lower(hex(randomblob(8)))"); + builder.Apply(provider); + foreach (var table in new[] { "RawImperative", "RawFluent" }) + { + var defaultExpression = provider.GetColumns(table).Single(c => c.Name == "Token").DefaultValue; + Assert.That(defaultExpression, Is.TypeOf()); + provider.ChangeColumn(table, new Column("Id", DbType.Int64)); + provider.ExecuteNonQuery("INSERT INTO " + table + " (Id) VALUES (1)"); + Assert.That(provider.ExecuteScalar("SELECT length(Token) FROM " + table), Is.EqualTo(16)); + Assert.That(provider.ExecuteScalar("SELECT Literal FROM " + table), Is.EqualTo("lower(hex(randomblob(8)))")); + } + var preview = new MigrationBuilder(); + preview.Create.Table("RawPreview").WithColumn("Token").AsString(40) + .WithDefaultValue(RawSql.Insert("lower(hex(randomblob(8)))")); + provider.ExecuteNonQuery(preview.Preview(new SqlGenerationContext(ProviderTypes.SQLite)).Single()); + provider.ExecuteNonQuery("INSERT INTO RawPreview DEFAULT VALUES"); + Assert.That(provider.ExecuteScalar("SELECT length(Token) FROM RawPreview"), Is.EqualTo(16)); + } + + [Test] + public void SemanticCollationDoesNotSilentlyDowngradeUnicodeToAscii() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + Assert.Throws(() => provider.AddTable("UnicodeNames", + new Column("Name", DbType.String, 40) { Collation = Collation.CaseInsensitive })); + Assert.That(provider.TableExists("UnicodeNames"), Is.False); + var builder = new MigrationBuilder(); + builder.Create.Table("AsciiNames").WithColumn("Name").AsString(40).WithCollation(Collation.AsciiIgnoreCase) + .WithUniqueConstraint("UQ_Ascii", "Name"); + provider.ExecuteNonQuery(builder.Preview(new SqlGenerationContext(ProviderTypes.SQLite)).Single()); + provider.Insert("AsciiNames", ["Name"], ["hello"]); + Assert.Catch(() => provider.Insert("AsciiNames", ["Name"], ["HELLO"])); + provider.Insert("AsciiNames", ["Name"], ["é"]); + provider.Insert("AsciiNames", ["Name"], ["É"]); + Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT COUNT(*) FROM AsciiNames")), Is.EqualTo(3)); + Assert.Throws(() => provider.ChangeColumn("AsciiNames", + new Column("Name", DbType.String, 80) { Collation = Collation.AsciiIgnoreCase })); + Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT COUNT(*) FROM AsciiNames")), Is.EqualTo(3)); + } + [Test] public void InvalidKeyDefinitionsFailBeforeCreatingTheTable() { diff --git a/src/Migrator/Framework/Collation.cs b/src/Migrator/Framework/Collation.cs new file mode 100644 index 00000000..fd55c71a --- /dev/null +++ b/src/Migrator/Framework/Collation.cs @@ -0,0 +1,28 @@ +using System; + +namespace DotNetProjects.Migrator.Framework; + +/// Comparison intent, resolved by the dialect. Linguistic ordering, +/// normalization and trailing-space behavior remain database-specific. +public sealed record Collation +{ + public CollationKind Kind { get; } + public string Name { get; } + private Collation(CollationKind kind, string name = null) { Kind = kind; Name = name; } + public static Collation Binary { get; } = new(CollationKind.Binary); + /// Unicode, case-sensitive and accent-sensitive comparison. + public static Collation CaseSensitive { get; } = new(CollationKind.CaseSensitive); + /// Unicode, case-insensitive and accent-sensitive comparison. + public static Collation CaseInsensitive { get; } = new(CollationKind.CaseInsensitive); + /// Fold ASCII A-Z only. Does not request Unicode case folding. + public static Collation AsciiIgnoreCase { get; } = new(CollationKind.AsciiIgnoreCase); + public static Collation Named(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("A collation name is required.", nameof(name)); + return new(CollationKind.Named, name); + } + public static implicit operator Collation(string name) => name == null ? null : Named(name); + public override string ToString() => Name ?? Kind.ToString(); +} + +public enum CollationKind { Named, Binary, CaseSensitive, CaseInsensitive, AsciiIgnoreCase } diff --git a/src/Migrator/Framework/Column.cs b/src/Migrator/Framework/Column.cs index 291b0e67..d6a1a022 100644 --- a/src/Migrator/Framework/Column.cs +++ b/src/Migrator/Framework/Column.cs @@ -75,7 +75,7 @@ public Column(string name, MigratorDbType type, object defaultValue) public bool IsNullable { get; set; } = true; public bool IsUnsigned { get; set; } - public string Collation { get; set; } + public Collation Collation { get; set; } public string Name { get; set; } diff --git a/src/Migrator/Framework/Fluent/MigrationBuilder.cs b/src/Migrator/Framework/Fluent/MigrationBuilder.cs index 3d5aa881..d030c77d 100644 --- a/src/Migrator/Framework/Fluent/MigrationBuilder.cs +++ b/src/Migrator/Framework/Fluent/MigrationBuilder.cs @@ -81,7 +81,7 @@ public sealed class TableBuilder public TableBuilder NotNullable() { Current.IsNullable = false; return this; } public TableBuilder Nullable() { Current.IsNullable = true; return this; } public TableBuilder Unsigned() { Current.IsUnsigned = true; return this; } - public TableBuilder WithCollation(string name) { Current.Collation = name; return this; } + public TableBuilder WithCollation(Collation name) { Current.Collation = name; return this; } public TableBuilder Identity() { Current.IsIdentity = true; return this; } } public sealed class ColumnBuilder @@ -99,7 +99,7 @@ public sealed class ColumnBuilder public ColumnBuilder NotNullable() { column.IsNullable = false; return this; } public ColumnBuilder Nullable() { column.IsNullable = true; return this; } public ColumnBuilder Unsigned() { column.IsUnsigned = true; return this; } - public ColumnBuilder WithCollation(string name) { column.Collation = name; return this; } + public ColumnBuilder WithCollation(Collation name) { column.Collation = name; return this; } public ColumnBuilder Identity() { column.IsIdentity = true; return this; } } public sealed class AlterRoot(MigrationBuilder builder) diff --git a/src/Migrator/Framework/IColumn.cs b/src/Migrator/Framework/IColumn.cs index 87310b29..caafd50c 100644 --- a/src/Migrator/Framework/IColumn.cs +++ b/src/Migrator/Framework/IColumn.cs @@ -19,7 +19,7 @@ public interface IColumn { bool IsNullable { get; set; } bool IsUnsigned { get; set; } - string Collation { get; set; } + Collation Collation { get; set; } int? Precision { get; set; } int? Scale { get; set; } diff --git a/src/Migrator/Framework/IDialect.cs b/src/Migrator/Framework/IDialect.cs index b4de8f95..b46df7e3 100644 --- a/src/Migrator/Framework/IDialect.cs +++ b/src/Migrator/Framework/IDialect.cs @@ -6,6 +6,7 @@ public interface IDialect { string QuoteIdentifier(string name); string GetCollationSql(string name); + string GetCollationSql(Collation collation); string GetTableConstraintSql(TableConstraint constraint); diff --git a/src/Migrator/Framework/RawSql.cs b/src/Migrator/Framework/RawSql.cs new file mode 100644 index 00000000..5cd522b9 --- /dev/null +++ b/src/Migrator/Framework/RawSql.cs @@ -0,0 +1,19 @@ +using System; + +namespace DotNetProjects.Migrator.Framework; + +/// An explicit, trusted SQL expression used as a column default. +/// Expressions are provider-specific and are never quoted as string literals. +public sealed record RawSql +{ + public string Sql { get; } + private RawSql(string sql) + { + if (string.IsNullOrWhiteSpace(sql)) throw new ArgumentException("A SQL expression is required.", nameof(sql)); + Sql = sql; + } + + /// Insert an expression verbatim. Never pass untrusted input. + public static RawSql Insert(string sql) => new(sql); + public override string ToString() => Sql; +} diff --git a/src/Migrator/Providers/CatalogDefaultValue.cs b/src/Migrator/Providers/CatalogDefaultValue.cs index d8ada5be..79cd5ceb 100644 --- a/src/Migrator/Providers/CatalogDefaultValue.cs +++ b/src/Migrator/Providers/CatalogDefaultValue.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Globalization; +using DotNetProjects.Migrator.Framework; namespace DotNetProjects.Migrator.Providers; @@ -8,11 +9,6 @@ namespace DotNetProjects.Migrator.Providers; // from expression objects. Keep expressions unquoted when a column is recreated. internal static class CatalogDefaultValue { - private sealed record Expression(string Sql) - { - public override string ToString() => Sql; - } - internal static object Parse(string source, DbType type) { var value = source.Trim(); @@ -23,6 +19,7 @@ internal static object Parse(string source, DbType type) var literal = value[1..^1].Replace("''", "'"); if (type is DbType.Date or DbType.DateTime or DbType.DateTime2 && DateTime.TryParse(literal, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) return DateTime.SpecifyKind(date, DateTimeKind.Utc); + if (type == DbType.Guid && Guid.TryParse(literal, out var guid)) return guid; return literal; } if (type == DbType.Boolean) @@ -30,6 +27,10 @@ internal static object Parse(string source, DbType type) if (bool.TryParse(value, out var boolean)) return boolean; if (value is "0" or "1") return value == "1"; } + if (type == DbType.SByte && sbyte.TryParse(value, CultureInfo.InvariantCulture, out var signedByte)) return signedByte; + if (type == DbType.UInt16 && ushort.TryParse(value, CultureInfo.InvariantCulture, out var unsignedSmall)) return unsignedSmall; + if (type == DbType.UInt32 && uint.TryParse(value, CultureInfo.InvariantCulture, out var unsignedInteger)) return unsignedInteger; + if (type == DbType.UInt64 && ulong.TryParse(value, CultureInfo.InvariantCulture, out var unsignedLarge)) return unsignedLarge; if (type == DbType.Byte && byte.TryParse(value, CultureInfo.InvariantCulture, out var tiny)) return tiny; if (type == DbType.Int16 && short.TryParse(value, CultureInfo.InvariantCulture, out var small)) return small; if (type == DbType.Int32 && int.TryParse(value, CultureInfo.InvariantCulture, out var integer)) return integer; @@ -37,7 +38,7 @@ internal static object Parse(string source, DbType type) if (type is DbType.Decimal or DbType.VarNumeric or DbType.Currency && decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) return number; if (type == DbType.Double && double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var floating)) return floating; if (type == DbType.Single && float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var real)) return real; - return new Expression(value); + return RawSql.Insert(source.Trim()); } private static bool HasOuterParentheses(string value) diff --git a/src/Migrator/Providers/ColumnPropertiesMapper.cs b/src/Migrator/Providers/ColumnPropertiesMapper.cs index aef0d500..5b4e0a3c 100644 --- a/src/Migrator/Providers/ColumnPropertiesMapper.cs +++ b/src/Migrator/Providers/ColumnPropertiesMapper.cs @@ -32,8 +32,12 @@ private void Map(Column column, bool includeDefault) } protected virtual void AddCollation(Column column, List values) { - if (!string.IsNullOrWhiteSpace(column.Collation)) + if (column.Collation != null) + { + if (column.Type is not (System.Data.DbType.String or System.Data.DbType.AnsiString or System.Data.DbType.StringFixedLength or System.Data.DbType.AnsiStringFixedLength)) + throw new NotSupportedException("Collation requires a text column."); values.Add(_Dialect.GetCollationSql(column.Collation)); + } } protected virtual void AddDefaultValue(Column column, List values) { diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index 91ec754f..dd051307 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -36,6 +36,16 @@ protected Dialect() } /// Render a named table constraint without accessing a database. + public virtual string GetCollationSql(Collation collation) + { + if (collation == null) throw new ArgumentNullException(nameof(collation)); + return GetCollationSql(collation.Kind == CollationKind.Named ? collation.Name : ResolveCollation(collation.Kind)); + } + + /// Override to map semantic requests to collations installed on the target engine. + protected virtual string ResolveCollation(CollationKind kind) => + throw new NotSupportedException($"{GetType().Name} cannot resolve {kind}. Use Collation.Named with an installed collation; no weaker comparison is substituted."); + public virtual string GetCollationSql(string name) => throw new NotSupportedException("Column collations are not supported by this dialect."); public virtual string GetTableConstraintSql(TableConstraint constraint) @@ -395,6 +405,7 @@ public virtual string QuoteTableNameIfRequired(string tableName) public virtual string Default(object defaultValue) { + if (defaultValue is RawSql expression) return "DEFAULT " + expression.Sql; if (defaultValue is string && defaultValue.ToString() == string.Empty) { defaultValue = "''"; diff --git a/src/Migrator/Providers/Impl/Mysql/MariaDBDialect.cs b/src/Migrator/Providers/Impl/Mysql/MariaDBDialect.cs index 2f8f614b..924dd0eb 100644 --- a/src/Migrator/Providers/Impl/Mysql/MariaDBDialect.cs +++ b/src/Migrator/Providers/Impl/Mysql/MariaDBDialect.cs @@ -5,6 +5,12 @@ namespace DotNetProjects.Migrator.Providers.Impl.Mysql; public class MariaDBDialect : MysqlDialect { + protected override string ResolveCollation(CollationKind kind) => kind switch + { + CollationKind.Binary => "utf8mb4_nopad_bin", CollationKind.CaseSensitive => "utf8mb4_uca1400_nopad_as_cs", CollationKind.CaseInsensitive => "utf8mb4_uca1400_nopad_as_ci", + _ => throw new System.NotSupportedException("MariaDB cannot resolve " + kind + ". Use an installed named collation.") + }; + public override ITransformationProvider GetTransformationProvider(Dialect dialect, string connectionString, string defaultSchema, string scope, string providerName) { return new MariaDBTransformationProvider(dialect, connectionString, scope, providerName); diff --git a/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs b/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs index 9874aeaa..d60d348f 100644 --- a/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs +++ b/src/Migrator/Providers/Impl/Mysql/MysqlDialect.cs @@ -5,6 +5,12 @@ namespace DotNetProjects.Migrator.Providers.Impl.Mysql; public class MysqlDialect : Dialect { + protected override string ResolveCollation(CollationKind kind) => kind switch + { + CollationKind.Binary => "utf8mb4_0900_bin", CollationKind.CaseSensitive => "utf8mb4_0900_as_cs", CollationKind.CaseInsensitive => "utf8mb4_0900_as_ci", + _ => base.ResolveCollation(kind) + }; + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); public MysqlDialect() diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs index 2a5e6bfd..1cd7fea1 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs @@ -6,6 +6,12 @@ namespace DotNetProjects.Migrator.Providers.Impl.PostgreSQL; public class PostgreSQLDialect : Dialect { + protected override string ResolveCollation(CollationKind kind) => kind switch + { + CollationKind.Binary => "C", + _ => base.ResolveCollation(kind) + }; + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); public PostgreSQLDialect() diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs index 00ba6dd7..63fe8266 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteDialect.cs @@ -5,6 +5,12 @@ namespace DotNetProjects.Migrator.Providers.Impl.SQLite; public class SQLiteDialect : Dialect { + protected override string ResolveCollation(CollationKind kind) => kind switch + { + CollationKind.Binary => "BINARY", CollationKind.AsciiIgnoreCase => "NOCASE", + _ => base.ResolveCollation(kind) + }; + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); public SQLiteDialect() @@ -59,6 +65,7 @@ public SQLiteDialect() public override string Default(object defaultValue) { + if (defaultValue is RawSql expression) return "DEFAULT (" + expression.Sql + ")"; if (defaultValue is bool) { return string.Format("DEFAULT {0}", (bool)defaultValue ? "1" : "0"); diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 02785fee..a21e8646 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1150,68 +1150,8 @@ public override Column[] GetColumns(string tableName) var defValue = pragmaTableInfoItem.DfltValue == DBNull.Value ? null : pragmaTableInfoItem.DfltValue; - if (defValue is string v && v.StartsWith("'") && v.EndsWith("'")) - { - column.DefaultValue = v.Substring(1, v.Length - 2); - } - else - { - column.DefaultValue = defValue; - } - - if (column.DefaultValue != null) - { - if (column.Type == DbType.Int16 || column.Type == DbType.Int32 || column.Type == DbType.Int64) - { - column.DefaultValue = long.Parse(column.DefaultValue.ToString()); - } - else if (column.Type == DbType.UInt16 || column.Type == DbType.UInt32 || column.Type == DbType.UInt64) - { - column.DefaultValue = ulong.Parse(column.DefaultValue.ToString()); - } - else if (column.Type == DbType.Double || column.Type == DbType.Single) - { - column.DefaultValue = double.Parse(column.DefaultValue.ToString()); - } - else if (column.Type == DbType.Boolean) - { - column.DefaultValue = column.DefaultValue.ToString().Trim() == "1" || column.DefaultValue.ToString().Trim().ToUpper() == "TRUE"; - } - else if (column.Type == DbType.DateTime || column.Type == DbType.DateTime2) - { - if (column.DefaultValue is string defVal) - { - var dt = defVal; - - if (defVal.StartsWith("'")) - { - dt = defVal.Substring(1, defVal.Length - 2); - } - - var d = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); - column.DefaultValue = d; - } - } - else if (column.Type == DbType.Guid) - { - if (column.DefaultValue is string defVal) - { - var dt = defVal; - - if (defVal.StartsWith("'")) - { - dt = defVal.Substring(1, defVal.Length - 2); - } - - var d = Guid.Parse(dt); - column.DefaultValue = d; - } - } - else if (column.Type == DbType.Boolean) - { - throw new NotSupportedException("SQLite does not support default values for BLOB columns."); - } - } + column.DefaultValue = defValue is string sqlDefault + ? CatalogDefaultValue.Parse(sqlDefault, column.Type) : defValue; var tableScript = GetSqlCreateTableScript(tableName); diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs index 16adc353..306f1f69 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs @@ -6,6 +6,12 @@ namespace DotNetProjects.Migrator.Providers.Impl.SqlServer; public class SqlServerDialect : Dialect { + protected override string ResolveCollation(CollationKind kind) => kind switch + { + CollationKind.Binary => "Latin1_General_100_BIN2", CollationKind.CaseSensitive => "Latin1_General_100_CS_AS_SC", CollationKind.CaseInsensitive => "Latin1_General_100_CI_AS_SC", + _ => base.ResolveCollation(kind) + }; + public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); public override bool NeedsNullForNullableWhenAlteringTable => true; From 2504c158fb08b7ba5a4b2183e853c62e56c70856 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:11:52 +0200 Subject: [PATCH 24/34] Render SQL Server collations as validated T-SQL tokens The new real SQL Server collation/default regression exposed that SQL Server rejects bracket-delimited collation names in CREATE TABLE. Validate the allowed collation-name token before emitting it without identifier brackets, preserving injection rejection for named inputs and semantic presets. Validation: solution build passes. Regression is the existing case/accent and NEWID test that failed in PR #181 run 35764844526; verify its new CI result. --- src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs index 306f1f69..cdb578b7 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerDialect.cs @@ -12,7 +12,13 @@ public class SqlServerDialect : Dialect _ => base.ResolveCollation(kind) }; - public override string GetCollationSql(string name) => "COLLATE " + QuoteIdentifier(name); + public override string GetCollationSql(string name) + { + // T-SQL requires a collation token rather than a bracket-delimited identifier. + if (string.IsNullOrWhiteSpace(name) || !System.Text.RegularExpressions.Regex.IsMatch(name, @"\A[A-Za-z][A-Za-z0-9_]*\z")) + throw new ArgumentException("SQL Server requires an unquoted collation name containing letters, digits and underscores.", nameof(name)); + return "COLLATE " + name; + } public override bool NeedsNullForNullableWhenAlteringTable => true; From 662ee0b85a45b8dd76e6d80788dda88a8c36d737 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:17:24 +0200 Subject: [PATCH 25/34] Preserve raw default expressions in SQL Server, Oracle and PostgreSQL metadata Catalog inspection previously attempted to parse function expressions as CLR numbers, GUIDs or dates, or returned them as quoted text during schema copying. Recognize expression defaults before literal-specific conversion and retain them as RawSql. Skip PostgreSQL identity defaults and preserve existing supported literal casts. Decode escaped string literals without stripping their contents. Add a live regression on all three engines that creates LOWER('ABC') as an expression and as a literal, inspects metadata, recreates a table from its column definitions, and checks persisted results from imperative and fluent creation. Validation: solution build passes. PR CI verifies the new round trips against the real engines; this is a new check beyond the earlier green v13 database matrix. --- .../Providers/Generic/RawDefaultRegression.cs | 33 +++++++++++++++++++ ...racleTransformationProviderGenericTests.cs | 3 ++ ...TransformationProvider_GetColumns_Tests.cs | 3 ++ ...erverTransformationProviderGenericTests.cs | 3 ++ src/Migrator/Providers/CatalogDefaultValue.cs | 1 + .../Oracle/OracleTransformationProvider.cs | 7 +++- .../PostgreSQLTransformationProvider.cs | 17 ++++++++-- .../SqlServerTransformationProvider.cs | 7 +++- 8 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/Migrator.Tests/Providers/Generic/RawDefaultRegression.cs diff --git a/src/Migrator.Tests/Providers/Generic/RawDefaultRegression.cs b/src/Migrator.Tests/Providers/Generic/RawDefaultRegression.cs new file mode 100644 index 00000000..6fe51efa --- /dev/null +++ b/src/Migrator.Tests/Providers/Generic/RawDefaultRegression.cs @@ -0,0 +1,33 @@ +using System; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Fluent; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.Generic; + +internal static class RawDefaultRegression +{ + internal static void AssertRoundTrip(ITransformationProvider provider) + { + provider.AddTable("RawDefaultsSource", new Column("Id", DbType.Int32), + new Column("ExpressionValue", DbType.String, 50) { DefaultValue = RawSql.Insert("LOWER('ABC')") }, + new Column("LiteralValue", DbType.String, 50) { DefaultValue = "LOWER('ABC')" }); + var columns = provider.GetColumns("RawDefaultsSource"); + Assert.That(columns.Single(c => c.Name.Equals("ExpressionValue", StringComparison.OrdinalIgnoreCase)).DefaultValue, Is.TypeOf()); + provider.AddTable("RawDefaultsCopy", columns); + var builder = new MigrationBuilder(); + builder.Create.Table("RawDefaultsFluent").WithColumn("Id").AsInt32() + .WithColumn("ExpressionValue").AsString(50).WithDefaultValue(RawSql.Insert("LOWER('ABC')")) + .WithColumn("LiteralValue").AsString(50).WithDefaultValue("LOWER('ABC')"); + builder.Apply(provider); + foreach (var table in new[] { "RawDefaultsSource", "RawDefaultsCopy", "RawDefaultsFluent" }) + { + provider.Insert(table, ["Id"], [1]); + var quoted = provider.QuoteTableNameIfRequired(table); + Assert.That(provider.ExecuteScalar("SELECT " + provider.QuoteColumnNameIfRequired("ExpressionValue") + " FROM " + quoted), Is.EqualTo("abc")); + Assert.That(provider.ExecuteScalar("SELECT " + provider.QuoteColumnNameIfRequired("LiteralValue") + " FROM " + quoted), Is.EqualTo("LOWER('ABC')")); + } + } +} diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs index aac7a5eb..da019243 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs @@ -10,6 +10,9 @@ namespace Migrator.Tests.Providers.OracleProvider; [Category("Oracle")] public class OracleTransformationProviderGenericTests : TransformationProviderGenericMiscConstraintBase { + [Test] + public void RawSqlDefaultsRoundTripThroughMetadata() => RawDefaultRegression.AssertRoundTrip(Provider); + [SetUp] public async Task SetUpAsync() { diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_Tests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_Tests.cs index 26613b31..fa2d40cf 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_Tests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_Tests.cs @@ -8,6 +8,9 @@ namespace Migrator.Tests.Providers.PostgreSQL; [Category("PostgreSQL")] public class PostgreSQLTransformationProvider_GetColumns_Tests : Generic_GetColumnsTestsBase { + [Test] + public void RawSqlDefaultsRoundTripThroughMetadata() => RawDefaultRegression.AssertRoundTrip(Provider); + [SetUp] public async Task SetUpAsync() { diff --git a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs index a33d2f98..4a6b57d3 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs @@ -12,6 +12,9 @@ namespace Migrator.Tests.Providers.SQLServer; [Category("SQLServer")] public class SqlServerTransformationProviderGenericTests : TransformationProviderGenericMiscConstraintBase { + [Test] + public void RawSqlDefaultsRoundTripThroughMetadata() => RawDefaultRegression.AssertRoundTrip(Provider); + [SetUp] public async Task SetUpAsync() { diff --git a/src/Migrator/Providers/CatalogDefaultValue.cs b/src/Migrator/Providers/CatalogDefaultValue.cs index 79cd5ceb..f7f23025 100644 --- a/src/Migrator/Providers/CatalogDefaultValue.cs +++ b/src/Migrator/Providers/CatalogDefaultValue.cs @@ -14,6 +14,7 @@ internal static object Parse(string source, DbType type) var value = source.Trim(); while (HasOuterParentheses(value)) value = value[1..^1].Trim(); if (value.Equals("NULL", StringComparison.OrdinalIgnoreCase)) return null; + if (value.StartsWith("N'", StringComparison.OrdinalIgnoreCase)) value = value[1..]; if (value.StartsWith("'") && value.EndsWith("'")) { var literal = value[1..^1].Replace("''", "'"); diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index e79fa2e7..f0654a75 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -606,7 +606,12 @@ public override Column[] GetColumns(string table) // This is only necessary because older versions of this migrator added single quotes for numerics. var singleQuoteStrippedString = dataDefaultString.Replace("'", ""); - if (column.Type == DbType.Int16 || column.Type == DbType.Int32 || column.Type == DbType.Int64) + var parsedDefault = CatalogDefaultValue.Parse(dataDefaultString, column.Type); + if (column.Type is DbType.String or DbType.AnsiString or DbType.StringFixedLength or DbType.AnsiStringFixedLength + || (parsedDefault is RawSql && !Regex.IsMatch(dataDefaultString, + @"(?i)^\s*(TO_TIMESTAMP\s*\(|TIMESTAMP\s*'|HEXTORAW\s*\()"))) + column.DefaultValue = parsedDefault; + else if (column.Type == DbType.Int16 || column.Type == DbType.Int32 || column.Type == DbType.Int64) { column.DefaultValue = long.Parse(singleQuoteStrippedString, CultureInfo.InvariantCulture); } diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index 346f9f52..25ab5284 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -615,9 +615,22 @@ public override Column[] GetColumns(string table) column.IsIdentity = true; } - if (columnInfo.ColumnDefault != null) + if (columnInfo.ColumnDefault != null && !isIdentity) { - if (column.MigratorDbType == MigratorDbType.Int16 || column.MigratorDbType == MigratorDbType.Int32 || column.MigratorDbType == MigratorDbType.Int64) + // Catalog casts on literal values retain the existing CLR conversion. + // All other expressions must survive inspection without evaluation or quoting. + var parsedDefault = CatalogDefaultValue.Parse(columnInfo.ColumnDefault, column.Type); + var isCastLiteral = Regex.IsMatch(columnInfo.ColumnDefault, + @"\A'(?:[^']|'')*'(?:::[A-Za-z0-9_ .\[\](),]+)?\z"); + if (column.Type is DbType.String or DbType.AnsiString or DbType.StringFixedLength or DbType.AnsiStringFixedLength) + { + var literal = Regex.Match(columnInfo.ColumnDefault, @"\A('(?:[^']|'')*')(?:::[A-Za-z0-9_ .\[\](),]+)?\z"); + column.DefaultValue = literal.Success + ? CatalogDefaultValue.Parse(literal.Groups[1].Value, column.Type) : parsedDefault; + } + else if (parsedDefault is RawSql && !isCastLiteral) + column.DefaultValue = parsedDefault; + else if (column.MigratorDbType == MigratorDbType.Int16 || column.MigratorDbType == MigratorDbType.Int32 || column.MigratorDbType == MigratorDbType.Int64) { var match = stripSingleQuoteRegEx.Match(columnInfo.ColumnDefault); if (match.Success) diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 6f4ab238..f62b550b 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -650,7 +650,12 @@ public override Column[] GetColumns(string table) var bracesStrippedString = defaultValueString.Replace("(", "").Replace(")", "").Trim(); var bracesAndSingleQuoteStrippedString = bracesStrippedString.Replace("'", ""); - if (column.Type == DbType.Int16 || column.Type == DbType.Int32 || column.Type == DbType.Int64) + var parsedDefault = CatalogDefaultValue.Parse(defaultValueString, column.Type); + if (column.Type is DbType.String or DbType.AnsiString or DbType.StringFixedLength or DbType.AnsiStringFixedLength + || (parsedDefault is RawSql && !System.Text.RegularExpressions.Regex.IsMatch(defaultValueString, + @"(?i)^\(*\s*(CONVERT\s*\(|0x[0-9a-f]+\)*)"))) + column.DefaultValue = parsedDefault; + else if (column.Type == DbType.Int16 || column.Type == DbType.Int32 || column.Type == DbType.Int64) { column.DefaultValue = long.Parse(bracesAndSingleQuoteStrippedString, CultureInfo.InvariantCulture); } From e606a39b661033594093df7b7ba579a656ffcbdb Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:20:59 +0200 Subject: [PATCH 26/34] Handle expression boundaries and adjacent comments in SQLite schema inspection Treat a quoted default as a string literal only when the entire expression is one escaped SQL literal. Concatenated defaults such as 'A' || 'B' remain RawSql and retain executable meaning when a table is reconstructed. Stop unquoted DDL tokens before adjacent line/block comments so primary-key keywords and constraint names are not lost when external schemas omit whitespace. Validation: rebuilt solution; all 197 SQLite tests pass, including new behavioral regressions for concatenated defaults and comment-adjacent named keys through a rebuild, persisted values and duplicate-key enforcement. --- src/Migrator.Tests/SchemaConstraintTests.cs | 24 +++++++++++++++++++ src/Migrator/Providers/CatalogDefaultValue.cs | 2 +- .../Impl/SQLite/SQLiteConstraintParser.cs | 3 ++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Migrator.Tests/SchemaConstraintTests.cs b/src/Migrator.Tests/SchemaConstraintTests.cs index ac63d5e0..e512a9a7 100644 --- a/src/Migrator.Tests/SchemaConstraintTests.cs +++ b/src/Migrator.Tests/SchemaConstraintTests.cs @@ -173,6 +173,30 @@ public void SemanticCollationDoesNotSilentlyDowngradeUnicodeToAscii() Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT COUNT(*) FROM AsciiNames")), Is.EqualTo(3)); } + [Test] + public void MetadataDoesNotConfuseConcatenatedExpressionsWithStringLiterals() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.AddTable("ConcatDefault", new Column("Id", DbType.Int32), + new Column("Value", DbType.String, 30) { DefaultValue = RawSql.Insert("'A' || 'B'") }); + Assert.That(provider.GetColumns("ConcatDefault").Single(c => c.Name == "Value").DefaultValue, Is.TypeOf()); + provider.ChangeColumn("ConcatDefault", new Column("Id", DbType.Int64)); + provider.ExecuteNonQuery("INSERT INTO ConcatDefault (Id) VALUES (1)"); + Assert.That(provider.ExecuteScalar("SELECT Value FROM ConcatDefault"), Is.EqualTo("AB")); + } + + [Test] + public void ConstraintTokenizerRecognizesCommentsAdjacentToKeywords() + { + using var provider = ProviderFactory.Create(ProviderTypes.SQLite, "Data Source=:memory:", null); + provider.ExecuteNonQuery("CREATE TABLE CommentedKey (Id INTEGER NOT NULL, Label TEXT, CONSTRAINT/*name*/pk PRIMARY/*kind*/KEY(Id))"); + Assert.That(provider.GetTableConstraints("CommentedKey").OfType().Single().Name, Is.EqualTo("pk")); + provider.ChangeColumn("CommentedKey", new Column("Label", DbType.String, 40)); + Assert.That(provider.GetTableConstraints("CommentedKey").OfType().Single().Name, Is.EqualTo("pk")); + provider.Insert("CommentedKey", ["Id"], [1]); + Assert.Catch(() => provider.Insert("CommentedKey", ["Id"], [1])); + } + [Test] public void InvalidKeyDefinitionsFailBeforeCreatingTheTable() { diff --git a/src/Migrator/Providers/CatalogDefaultValue.cs b/src/Migrator/Providers/CatalogDefaultValue.cs index f7f23025..422ee9e8 100644 --- a/src/Migrator/Providers/CatalogDefaultValue.cs +++ b/src/Migrator/Providers/CatalogDefaultValue.cs @@ -15,7 +15,7 @@ internal static object Parse(string source, DbType type) while (HasOuterParentheses(value)) value = value[1..^1].Trim(); if (value.Equals("NULL", StringComparison.OrdinalIgnoreCase)) return null; if (value.StartsWith("N'", StringComparison.OrdinalIgnoreCase)) value = value[1..]; - if (value.StartsWith("'") && value.EndsWith("'")) + if (System.Text.RegularExpressions.Regex.IsMatch(value, @"\A'(?:[^']|'')*'\z")) { var literal = value[1..^1].Replace("''", "'"); if (type is DbType.Date or DbType.DateTime or DbType.DateTime2 && DateTime.TryParse(literal, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs index 777153ee..c5c4f983 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteConstraintParser.cs @@ -175,7 +175,8 @@ private static List Tokenize(string sql) 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++; + while (i < sql.Length && !char.IsWhiteSpace(sql[i]) && sql[i] is not ('(' or ')' or ',' or '.' or '\'' or '"' or '`' or '[') && + !(i + 1 < sql.Length && ((sql[i] == '/' && sql[i + 1] == '*') || (sql[i] == '-' && sql[i + 1] == '-')))) i++; result.Add(new Token(sql[start..i], start, i)); } } From 11d6083927c29382c8166bfef238dfb8e2cb0e77 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:24:05 +0200 Subject: [PATCH 27/34] Preserve constraint details and reject ignored Oracle index options Read SQL Server nonclustered primary-key metadata from the backing index type. Use a parameterized Oracle foreign-key catalog reader with schema filtering, position-aligned composite keys and independent delete/update action reporting. Copy caller arrays in foreign-key constructors consistently with other keys. Reject Oracle included-column and clustered-index requests that were previously ignored silently. Add the corresponding migration-guide entry so callers can choose an ordinary index or an explicit Oracle-specific operation. Validation: solution build passes. Add live SQL Server nonclustered-key round-trip, Oracle composite-FK/action metadata, and Oracle unsupported-index diagnostics. The provider-specific CI results must pass before this follow-up is accepted. --- docs/migration-guide-12.1-to-13.md | 11 ++++++ ...racleTransformationProviderGenericTests.cs | 22 +++++++++++ ...erverTransformationProviderGenericTests.cs | 9 +++++ .../Framework/ForeignKeyConstraint.cs | 4 +- .../Providers/ConstraintMetadataReader.cs | 5 ++- .../Providers/ForeignKeyMetadataReader.cs | 19 +++++++++- .../Oracle/OracleTransformationProvider.cs | 37 ++----------------- 7 files changed, 69 insertions(+), 38 deletions(-) diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index 39d89783..0acc1b5b 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -196,3 +196,14 @@ database objects while rendering a column or preview. MySQL/MariaDB expose unique indexes as unique constraints in their catalogs, so metadata cannot recover whether the original author used CREATE UNIQUE INDEX or a UNIQUE table clause. No ownership decision may be inferred from that syntax. + +## Oracle index options and constraint metadata + +Oracle now rejects nonempty `Index.IncludeColumns` and `Index.Clustered = true` before +DDL. Version 12.1 silently ignored them. Remove these options for an ordinary Oracle +index or author an explicit Oracle-specific design; a SQL Server clustered-index +request is not translated to an Oracle index-organized table. + +Structured metadata preserves SQL Server nonclustered primary keys and Oracle +ordered foreign-key pairs/delete actions. Foreign-key constructor arrays are copied, +matching primary/unique definitions, so later caller-array edits cannot change the key. diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs index da019243..669c6fcf 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs @@ -13,6 +13,28 @@ public class OracleTransformationProviderGenericTests : TransformationProviderGe [Test] public void RawSqlDefaultsRoundTripThroughMetadata() => RawDefaultRegression.AssertRoundTrip(Provider); + [Test] + public void ForeignKeyMetadataPreservesOrderedPairsAndDeleteAction() + { + Provider.AddTable("MetaParents", new Column("FirstId", DbType.Int32), new Column("SecondId", DbType.Int32), + new PrimaryKeyConstraint("PK_MetaParents", "SecondId", "FirstId")); + Provider.AddTable("MetaChildren", new Column("LeftId", DbType.Int32), new Column("RightId", DbType.Int32)); + Provider.AddForeignKey("FK_MetaPair", "MetaChildren", ["LeftId", "RightId"], "MetaParents", ["SecondId", "FirstId"], ForeignKeyConstraintType.Cascade); + var key = System.Linq.Enumerable.Single(Provider.GetForeignKeyConstraints("MetaChildren")); + Assert.That(key.ChildColumns, Is.EqualTo(new[] { "LEFTID", "RIGHTID" })); + Assert.That(key.ParentColumns, Is.EqualTo(new[] { "SECONDID", "FIRSTID" })); + Assert.That(key.OnDelete, Is.EqualTo("CASCADE")); + Assert.That(key.OnUpdate, Is.EqualTo("NO ACTION")); + } + + [Test] + public void UnsupportedIndexOptionsFailExplicitly() + { + Assert.Throws(() => Provider.AddIndex("TestTwo", + new DotNetProjects.Migrator.Framework.Index { Name = "IX_Unsupported", KeyColumns = ["Id"], IncludeColumns = ["TestId"] })); + Assert.That(Provider.IndexExists("TestTwo", "IX_Unsupported"), Is.False); + } + [SetUp] public async Task SetUpAsync() { diff --git a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs index 4a6b57d3..5ac1d1fe 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs @@ -15,6 +15,15 @@ public class SqlServerTransformationProviderGenericTests : TransformationProvide [Test] public void RawSqlDefaultsRoundTripThroughMetadata() => RawDefaultRegression.AssertRoundTrip(Provider); + [Test] + public void NonClusteredPrimaryKeyRoundTripsAsConstraint() + { + Provider.AddTable("NonClusteredKey", new Column("Id", DbType.Int32), + new PrimaryKeyConstraint("PK_NonClusteredKey", "Id") { NonClustered = true }); + var key = System.Linq.Enumerable.Single(System.Linq.Enumerable.OfType(Provider.GetTableConstraints("NonClusteredKey"))); + Assert.That(key.NonClustered, Is.True); + } + [SetUp] public async Task SetUpAsync() { diff --git a/src/Migrator/Framework/ForeignKeyConstraint.cs b/src/Migrator/Framework/ForeignKeyConstraint.cs index 15d9d4a2..44569fed 100644 --- a/src/Migrator/Framework/ForeignKeyConstraint.cs +++ b/src/Migrator/Framework/ForeignKeyConstraint.cs @@ -9,9 +9,9 @@ public ForeignKeyConstraint(string name, string parentTable, string[] parentcolu { Name = name; ParentTable = parentTable; - ParentColumns = parentcolumns; + ParentColumns = (string[])parentcolumns.Clone(); ChildTable = childTable; - ChildColumns = childColumns; + ChildColumns = (string[])childColumns.Clone(); } /// diff --git a/src/Migrator/Providers/ConstraintMetadataReader.cs b/src/Migrator/Providers/ConstraintMetadataReader.cs index 2d5f8dd5..342facf2 100644 --- a/src/Migrator/Providers/ConstraintMetadataReader.cs +++ b/src/Migrator/Providers/ConstraintMetadataReader.cs @@ -22,8 +22,8 @@ public static TableConstraint[] Read(TransformationProvider provider, string tab 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 + sql = @"SELECT kc.name, CASE WHEN kc.type='PK' AND ix.type=2 THEN 'PN' ELSE kc.type END, c.name, ic.key_ordinal, CAST(NULL AS nvarchar(max)) + FROM sys.key_constraints kc JOIN sys.indexes ix ON ix.object_id=kc.parent_object_id AND ix.index_id=kc.unique_index_id 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(@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) @@ -101,6 +101,7 @@ void Complete() current = reader.GetString(1).Trim().ToUpperInvariant() switch { "P" or "PK" or "PRIMARY KEY" => new PrimaryKeyConstraint { Name = name }, + "PN" => new PrimaryKeyConstraint { Name = name, NonClustered = true }, "U" or "UQ" or "UNIQUE" => new UniqueConstraint { Name = name }, "C" or "K" or "CHECK" => new CheckConstraint(name, reader.IsDBNull(4) ? null : CheckExpression(reader.GetString(4))), _ => throw new MigrationException("Unknown catalog constraint type.") diff --git a/src/Migrator/Providers/ForeignKeyMetadataReader.cs b/src/Migrator/Providers/ForeignKeyMetadataReader.cs index 5929308d..0f76ca6a 100644 --- a/src/Migrator/Providers/ForeignKeyMetadataReader.cs +++ b/src/Migrator/Providers/ForeignKeyMetadataReader.cs @@ -8,6 +8,7 @@ using DotNetProjects.Migrator.Providers.Impl.Mysql; using DotNetProjects.Migrator.Providers.Impl.DB2; using DotNetProjects.Migrator.Providers.Impl.Firebird; +using DotNetProjects.Migrator.Providers.Impl.Oracle; using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; namespace DotNetProjects.Migrator.Providers; @@ -47,6 +48,22 @@ FROM information_schema.KEY_COLUMN_USAGE k JOIN information_schema.REFERENTIAL_C WHERE k.TABLE_NAME=@lookup_table AND k.TABLE_SCHEMA=COALESCE(@lookup_schema,DATABASE()) AND k.REFERENCED_TABLE_NAME IS NOT NULL ORDER BY k.CONSTRAINT_NAME,k.ORDINAL_POSITION"; } + else if (provider.Dialect is OracleDialect) + { + var parts = table.Split('.'); + if (parts.Length > 2 || parts.Any(p => p.Contains('"'))) + throw new NotSupportedException("Use unquoted schema/table names for Oracle foreign-key catalog lookup."); + parameterTable = parts[^1].ToUpperInvariant(); schema = parts.Length == 2 ? parts[0].ToUpperInvariant() : null; + sql = @"SELECT c.CONSTRAINT_NAME, + CASE WHEN p.OWNER=c.OWNER THEN p.TABLE_NAME ELSE p.OWNER||'.'||p.TABLE_NAME END, + cc.COLUMN_NAME,pc.COLUMN_NAME,cc.POSITION,c.DELETE_RULE,'NO ACTION' + FROM ALL_CONSTRAINTS c JOIN ALL_CONS_COLUMNS cc ON cc.OWNER=c.OWNER AND cc.CONSTRAINT_NAME=c.CONSTRAINT_NAME + JOIN ALL_CONSTRAINTS p ON p.OWNER=c.R_OWNER AND p.CONSTRAINT_NAME=c.R_CONSTRAINT_NAME + JOIN ALL_CONS_COLUMNS pc ON pc.OWNER=p.OWNER AND pc.CONSTRAINT_NAME=p.CONSTRAINT_NAME AND pc.POSITION=cc.POSITION + WHERE c.CONSTRAINT_TYPE='R' AND c.TABLE_NAME=:lookup_table + AND c.OWNER=COALESCE(:lookup_schema,SYS_CONTEXT('USERENV','CURRENT_SCHEMA')) + ORDER BY c.CONSTRAINT_NAME,cc.POSITION"; + } else if (provider.Dialect is DB2Dialect) { parameterTable = table.StartsWith('"') ? table.Trim('"') : table.ToUpperInvariant(); @@ -69,7 +86,7 @@ FROM information_schema.KEY_COLUMN_USAGE k JOIN information_schema.REFERENTIAL_C else throw new NotSupportedException("Foreign-key metadata is unsupported by " + provider.Dialect.GetType().Name + "."); using var command = provider.CreateCommand(); AddParameter(command, "lookup_table", parameterTable); - if (provider.Dialect is MysqlDialect) AddParameter(command, "lookup_schema", schema); + if (provider.Dialect is MysqlDialect or OracleDialect) AddParameter(command, "lookup_schema", schema); var rows = new List<(string Name, string Parent, string ChildColumn, string ParentColumn, string Delete, string Update)>(); using (var reader = provider.ExecuteQuery(command, sql)) while (reader.Read()) diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index f0654a75..5e7b5f71 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -56,38 +56,8 @@ public override void DropDatabases(string databaseName) } } - public override ForeignKeyConstraint[] GetForeignKeyConstraints(string table) - { - var constraints = new List(); - var foreignKeyConstraintItems = _oracleSystemDataLoader.GetForeignKeyConstraintItems(table); - - var schemaChildTableGroups = foreignKeyConstraintItems.GroupBy(x => new { x.SchemaName, x.ChildTableName }).Count(); - - if (schemaChildTableGroups > 1) - { - throw new MigrationException($"Duplicates found (grouping by schema name and child table name). Since we do not offer schemas in '{nameof(GetForeignKeyConstraints)}' at this moment in time we cannot filter your target schema. Your database use the same table name in different schemas."); - } - - var groups = foreignKeyConstraintItems.GroupBy(x => x.ForeignKeyName); - - foreach (var group in groups) - { - var first = group.First(); - - var foreignKeyConstraint = new ForeignKeyConstraint - { - Name = first.ForeignKeyName, - ParentTable = first.ParentTableName, - ParentColumns = [.. group.Select(x => x.ParentColumnName).Distinct()], - ChildTable = first.ChildTableName, - ChildColumns = [.. group.Select(x => x.ChildColumnName).Distinct()] - }; - - constraints.Add(foreignKeyConstraint); - } - - return [.. constraints]; - } + public override ForeignKeyConstraint[] GetForeignKeyConstraints(string table) => + ForeignKeyMetadataReader.Read(this, table); public override void AddForeignKey(string name, string primaryTable, string[] primaryColumns, string refTable, string[] refColumns, ForeignKeyConstraintType constraint) @@ -102,7 +72,8 @@ public override string AddIndex(string table, Index index) ValidateIndex(tableName: table, index: index); var hasFilterItems = index.FilterItems != null && index.FilterItems.Count > 0; - // Oracle does not support included columns and clustered indexes. We ignore the values given in the properties SILENTLY for backwards compatibility. + if (index.IncludeColumns?.Length > 0 || index.Clustered) + throw new NotSupportedException("Oracle does not support included columns or SQL Server-style clustered indexes. Use an explicit Oracle operation."); if (index.Unique && hasFilterItems) { From ac36162e376573161259500e8298b34edde16351 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:07:53 +0200 Subject: [PATCH 28/34] Qualify additional FluentMigrator engines against real CI infrastructure Compare current FluentMigrator runner projects with our database matrix. Record Redshift, Snowflake and Db2 for IBM i as deferred until real test endpoints and credentials exist; distinguish Db2 LUW and PostgreSQL from those engines and exclude providers dropped upstream. Add an isolated HANA Express qualification job using a pinned official container and the SAP .NET driver. Require real DDL, identity/constraint creation, persisted data, transaction rollback and native catalog queries, with bounded startup, failure artifacts and unconditional container cleanup. This probe makes no provider-support claim; admission requires passing CI and subsequent migration provider integration tests. Validation: qualification .NET project builds locally with no warnings or errors. Actual engine availability is deliberately validated in GitHub Actions. --- .github/qualification/Hana/Hana.csproj | 4 ++ .github/qualification/Hana/Program.cs | 39 ++++++++++++ .github/workflows/hana-qualification.yml | 72 +++++++++++++++++++++++ docs/additional-database-qualification.md | 45 ++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 .github/qualification/Hana/Hana.csproj create mode 100644 .github/qualification/Hana/Program.cs create mode 100644 .github/workflows/hana-qualification.yml create mode 100644 docs/additional-database-qualification.md diff --git a/.github/qualification/Hana/Hana.csproj b/.github/qualification/Hana/Hana.csproj new file mode 100644 index 00000000..6beacf72 --- /dev/null +++ b/.github/qualification/Hana/Hana.csproj @@ -0,0 +1,4 @@ + + Exenet9.0enable + + diff --git a/.github/qualification/Hana/Program.cs b/.github/qualification/Hana/Program.cs new file mode 100644 index 00000000..3050b6cd --- /dev/null +++ b/.github/qualification/Hana/Program.cs @@ -0,0 +1,39 @@ +using Sap.Data.Hana; + +using var connection = new HanaConnection(Environment.GetEnvironmentVariable("MIGRATOR_HANA") + ?? throw new InvalidOperationException("MIGRATOR_HANA must identify the disposable CI database.")); +connection.Open(); +void Execute(string sql) +{ + using var command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); +} +object Scalar(string sql) +{ + using var command = connection.CreateCommand(); + command.CommandText = sql; + return command.ExecuteScalar(); +} +Execute("CREATE SCHEMA MIGRATOR_QUALIFICATION"); +try +{ + Execute("CREATE ROW TABLE MIGRATOR_QUALIFICATION.ITEMS (ID INTEGER GENERATED BY DEFAULT AS IDENTITY, LABEL NVARCHAR(40) DEFAULT 'initial', CONSTRAINT PK_ITEMS PRIMARY KEY (ID), CONSTRAINT UQ_LABEL UNIQUE (LABEL))"); + Execute("INSERT INTO MIGRATOR_QUALIFICATION.ITEMS (LABEL) VALUES ('kept')"); + if (Convert.ToInt32(Scalar("SELECT COUNT(*) FROM MIGRATOR_QUALIFICATION.ITEMS")) != 1) + throw new Exception("Inserted row missing."); + using (var transaction = connection.BeginTransaction()) + { + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = "INSERT INTO MIGRATOR_QUALIFICATION.ITEMS (LABEL) VALUES ('rolled back')"; + command.ExecuteNonQuery(); + transaction.Rollback(); + } + if (Convert.ToInt32(Scalar("SELECT COUNT(*) FROM MIGRATOR_QUALIFICATION.ITEMS")) != 1) + throw new Exception("Rollback did not preserve the original row count."); + if (Convert.ToInt32(Scalar("SELECT COUNT(*) FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME='MIGRATOR_QUALIFICATION' AND TABLE_NAME='ITEMS'")) != 2) + throw new Exception("Column catalog could not be read."); + Console.WriteLine("HANA qualification passed: native .NET connection, DDL, identity, constraints, data, rollback and catalog access."); +} +finally { Execute("DROP SCHEMA MIGRATOR_QUALIFICATION CASCADE"); } diff --git a/.github/workflows/hana-qualification.yml b/.github/workflows/hana-qualification.yml new file mode 100644 index 00000000..577d5ade --- /dev/null +++ b/.github/workflows/hana-qualification.yml @@ -0,0 +1,72 @@ +name: SAP HANA qualification +on: + pull_request: + branches: [master, "codex/**"] + paths: + - ".github/workflows/hana-qualification.yml" + - ".github/qualification/Hana/**" + workflow_dispatch: +permissions: + contents: read +concurrency: + group: hana-qualification-${{ github.ref }} + cancel-in-progress: true +jobs: + hana: + name: Qualify actual SAP HANA engine and .NET driver + runs-on: ubuntu-22.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Start disposable HANA Express + timeout-minutes: 22 + shell: bash + run: | + set -euo pipefail + sudo sysctl -w fs.file-max=20000000 fs.aio-max-nr=262144 vm.memory_failure_early_kill=1 vm.max_map_count=135217728 + mkdir -p "$RUNNER_TEMP/hana" + printf '{"master_password":"MgT9ci7Q4xZ2"}' > "$RUNNER_TEMP/hana/password.json" + sudo chown -R 12000:79 "$RUNNER_TEMP/hana" + sudo chmod 600 "$RUNNER_TEMP/hana/password.json" + docker pull saplabs/hanaexpress:2.00.088.00.20251110.1 + docker run -d --name migrator-hana --hostname hxe -p 39041:39041 \ + --ulimit nofile=1048576:1048576 \ + --sysctl kernel.shmmax=1073741824 --sysctl kernel.shmmni=524288 --sysctl kernel.shmall=8388608 \ + --sysctl net.ipv4.ip_local_port_range="40000 60999" \ + -v "$RUNNER_TEMP/hana:/hana/mounts" \ + saplabs/hanaexpress:2.00.088.00.20251110.1 \ + --passwords-url file:///hana/mounts/password.json --agree-to-sap-license + for attempt in $(seq 1 180); do + if docker exec migrator-hana /usr/sap/HXE/HDB90/exe/hdbsql -i 90 -d HXE -u SYSTEM -p MgT9ci7Q4xZ2 'SELECT 1 FROM DUMMY' >/dev/null 2>&1; then + exit 0 + fi + if [ "$(docker inspect -f '{{.State.Running}}' migrator-hana)" != true ]; then + docker logs migrator-hana + exit 1 + fi + sleep 5 + done + docker logs migrator-hana + exit 1 + - name: Test actual engine through SAP .NET driver + env: + MIGRATOR_HANA: "Server=localhost:39041;UserID=SYSTEM;Password=MgT9ci7Q4xZ2" + run: dotnet run --project .github/qualification/Hana/Hana.csproj + - name: Collect evidence + if: always() + run: | + mkdir -p TestResults + docker logs migrator-hana > TestResults/hana.log 2>&1 || true + free -m > TestResults/memory.txt + df -h > TestResults/disk.txt + - uses: actions/upload-artifact@v4 + if: always() + with: + name: hana-qualification + path: TestResults/ + - name: Cleanup + if: always() + run: docker rm -fv migrator-hana || true diff --git a/docs/additional-database-qualification.md b/docs/additional-database-qualification.md new file mode 100644 index 00000000..56bae41b --- /dev/null +++ b/docs/additional-database-qualification.md @@ -0,0 +1,45 @@ +# Additional database qualification + +Reviewed 22 September 2026 against FluentMigrator's current +[runner projects](https://github.com/fluentmigrator/fluentmigrator/tree/main/src) +and [provider configuration](https://fluentmigrator.github.io/intro/configuration.html). +This is an implementation gate, not a claim of released provider support. + +| Additional engine | Real-engine GitHub Actions route | Current disposition | +| --- | --- | --- | +| SAP HANA | Official HANA Express Linux container and SAP's .NET driver, disposable schema | Qualification workflow added; provider admission requires a successful actual-engine run, followed by provider behavioral tests | +| Amazon Redshift | AWS test warehouse/serverless endpoint with CI credentials, network access and resource cleanup | No configured test infrastructure; defer provider | +| Snowflake | Snowflake test account, warehouse, credentials and disposable database/schema | No configured test infrastructure; defer provider | +| Db2 for IBM i | IBM i endpoint on Power infrastructure and compatible .NET/ODBC driver | No configured test infrastructure; defer provider | + +The existing Db2 job runs Db2 LUW, not Db2 for IBM i. PostgreSQL compatibility does +not prove Redshift behavior. Snowpark's local test framework does not test Snowflake +DDL and catalog behavior through the production .NET driver. + +Older provider lists also mention SQL Server Compact and SAP SQL Anywhere. +FluentMigrator's current FAQ marks these as dropped; they are not current additions +to pursue. Different SQL Server/PostgreSQL dialect versions and Oracle drivers are +not additional database engines. + +## HANA admission criteria + +The separate qualification workflow pins HANA Express 2.00.088.00.20251110.1 and +Sap.Data.Hana.Net.v8.0 2.30.27. It uses a disposable public CI credential, bounded +startup, native .NET connection, identity/constraint DDL, persisted data, rollback +and catalog checks. Any startup or behavioral failure fails the job. Logs and +runner resource evidence are retained; cleanup runs independently of test success. +This is a prerequisite probe, not provider integration coverage. + +If qualification passes, add the provider and mandatory matrix coverage for +imperative/fluent schema creation, constraint metadata, data, migration history, +restart/rollback, preview parity and explicit unsupported operations. Do not mark +a provider supported on the basis of SQL string tests or a skipped secret-gated job. + +## Primary sources + +- [SAP's official HANA Express image and installation requirements](https://hub.docker.com/r/saplabs/hanaexpress) +- [SAP Docker installation guide](https://developers.sap.com/tutorials/hxe-ua-install-using-docker) +- [Redshift Serverless setup](https://docs.aws.amazon.com/redshift/latest/gsg/) +- [Snowflake local testing framework scope](https://docs.snowflake.com/en/developer-guide/snowpark/python/testing-locally) +- [Db2 for IBM i platform](https://www.ibm.com/support/pages/db2-ibm-i) +- [FluentMigrator's current provider FAQ](https://fluentmigrator.github.io/intro/faq.html) From c42b0d157109d1432f47bbf3e04b5187dc3c0984 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:11:56 +0200 Subject: [PATCH 29/34] Use the supported Linux shared-memory segment limit for HANA qualification The first actual GitHub runner attempt pulled the official image successfully but OCI refused SAP's documented kernel.shmmni=524288 setting. Set the namespace limit to Linux's supported 32768 maximum so the container can start. Keep the actual-engine readiness and .NET behavior tests mandatory; no success is inferred from image availability. Evidence: qualification run 35765175072 failed at container creation, before any database tests. The next run must reach the real HANA checks. --- .github/workflows/hana-qualification.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/hana-qualification.yml b/.github/workflows/hana-qualification.yml index 577d5ade..cbdc6b7a 100644 --- a/.github/workflows/hana-qualification.yml +++ b/.github/workflows/hana-qualification.yml @@ -34,7 +34,7 @@ jobs: docker pull saplabs/hanaexpress:2.00.088.00.20251110.1 docker run -d --name migrator-hana --hostname hxe -p 39041:39041 \ --ulimit nofile=1048576:1048576 \ - --sysctl kernel.shmmax=1073741824 --sysctl kernel.shmmni=524288 --sysctl kernel.shmall=8388608 \ + --sysctl kernel.shmmax=1073741824 --sysctl kernel.shmmni=32768 --sysctl kernel.shmall=8388608 \ --sysctl net.ipv4.ip_local_port_range="40000 60999" \ -v "$RUNNER_TEMP/hana:/hana/mounts" \ saplabs/hanaexpress:2.00.088.00.20251110.1 \ From 0b854f616838fa807cace7957292ab0b65d10a0e Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:32:42 +0200 Subject: [PATCH 30/34] Add the SAP HANA provider after actual-engine CI qualification The pinned official HANA Express image and SAP .NET driver passed real DDL, data, transaction rollback and catalog access in run 35766200488. Add Hana dialect/provider registration, native type rendering, positional parameters, schema-aware catalog readers, explicit constraints/FK actions, ordinary indexes, column/table alterations, history support and structured create/add preview. Replace the prerequisite-only workflow with a mandatory Hana job in the complete database matrix. Add eight actual-engine tests covering owned/caller connections, imperative/fluent/preview behavior, constraints and metadata, schema qualification, data rollback, restart/downgrade/read-only planning and unsupported capabilities. Keep SAP dependencies in tests/hosts; the core has no driver package dependency. Do not claim transactional DDL, whole-session transactions, native locks, tenant administration or unimplemented specialized schema features. Document the exact provider scope and retain qualification evidence. Other additional FluentMigrator engines remain deferred until real CI infrastructure is available. Validation: solution build; 87 unit and 197 SQLite tests pass locally. The newly mandatory Hana matrix job must validate the provider itself before support is accepted; the prerequisite probe alone is not sufficient. --- .github/scripts/start-database.sh | 1 + .github/scripts/start-hana.sh | 28 +++ .github/scripts/test.ps1 | 6 +- .github/scripts/verify-test-coverage.py | 2 +- .github/workflows/dotnetpull.yml | 2 +- .github/workflows/hana-qualification.yml | 72 ------- docs/additional-database-qualification.md | 32 ++- src/Migrator.Tests/Migrator.Tests.csproj | 1 + .../Providers/Hana/HanaProviderTests.cs | 175 ++++++++++++++++ src/Migrator/Framework/Fluent/Operations.cs | 4 +- src/Migrator/ProviderFactory.cs | 2 + .../Providers/Impl/Hana/HanaDialect.cs | 58 ++++++ .../Impl/Hana/HanaTransformationProvider.cs | 194 ++++++++++++++++++ src/Migrator/Providers/ProviderTypes.cs | 1 + 14 files changed, 495 insertions(+), 83 deletions(-) create mode 100644 .github/scripts/start-hana.sh delete mode 100644 .github/workflows/hana-qualification.yml create mode 100644 src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs create mode 100644 src/Migrator/Providers/Impl/Hana/HanaDialect.cs create mode 100644 src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs diff --git a/.github/scripts/start-database.sh b/.github/scripts/start-database.sh index 7bb5d15a..b71442de 100644 --- a/.github/scripts/start-database.sh +++ b/.github/scripts/start-database.sh @@ -11,6 +11,7 @@ pull() { } case "$database" in Unit|SQLite) exit 0 ;; + Hana) bash .github/scripts/start-hana.sh; exit 0 ;; MySQL) docker run -d --name migrator-db -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=testdb -e MYSQL_USER=testuser -e MYSQL_PASSWORD=testpass mysql:8.0.44 ready() { docker exec migrator-db mysql -uroot -prootpass -e 'SELECT 1' >/dev/null 2>&1; } diff --git a/.github/scripts/start-hana.sh b/.github/scripts/start-hana.sh new file mode 100644 index 00000000..859c0217 --- /dev/null +++ b/.github/scripts/start-hana.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail +sudo sysctl -w fs.file-max=20000000 fs.aio-max-nr=262144 vm.memory_failure_early_kill=1 vm.max_map_count=135217728 +mkdir -p "$RUNNER_TEMP/hana" +printf '{"master_password":"MgT9ci7Q4xZ2"}' > "$RUNNER_TEMP/hana/password.json" +sudo chown -R 12000:79 "$RUNNER_TEMP/hana" +sudo chmod 600 "$RUNNER_TEMP/hana/password.json" +docker pull saplabs/hanaexpress:2.00.088.00.20251110.1 +docker run -d --name migrator-db --hostname hxe -p 39041:39041 \ + --ulimit nofile=1048576:1048576 \ + --sysctl kernel.shmmax=1073741824 --sysctl kernel.shmmni=32768 --sysctl kernel.shmall=8388608 \ + --sysctl net.ipv4.ip_local_port_range="40000 60999" \ + -v "$RUNNER_TEMP/hana:/hana/mounts" \ + saplabs/hanaexpress:2.00.088.00.20251110.1 \ + --passwords-url file:///hana/mounts/password.json --agree-to-sap-license +for attempt in $(seq 1 180); do + if docker exec migrator-db /usr/sap/HXE/HDB90/exe/hdbsql -i 90 -d HXE -u SYSTEM -p MgT9ci7Q4xZ2 'SELECT 1 FROM DUMMY' >/dev/null 2>&1; then + echo "MIGRATOR_HANA=Server=localhost:39041;UserID=SYSTEM;Password=MgT9ci7Q4xZ2" >> "$GITHUB_ENV" + exit 0 + fi + if [ "$(docker inspect -f '{{.State.Running}}' migrator-db)" != true ]; then + docker logs migrator-db + exit 1 + fi + sleep 5 +done +docker logs migrator-db +exit 1 diff --git a/.github/scripts/test.ps1 b/.github/scripts/test.ps1 index 01f97f7b..66a36a4b 100644 --- a/.github/scripts/test.ps1 +++ b/.github/scripts/test.ps1 @@ -1,9 +1,9 @@ param( - [ValidateSet('Unit','SQLite','SQLServer','PostgreSQL','Oracle','MySQL','MariaDB','Firebird','Db2','Informix','Sybase')] + [ValidateSet('Unit','SQLite','SQLServer','PostgreSQL','Oracle','MySQL','MariaDB','Firebird','Db2','Informix','Sybase','Hana')] [string]$Database = 'Unit' ) $ErrorActionPreference = 'Stop' -$databases = @('SQLite','SQLServer','PostgreSQL','Oracle','MySQL','MariaDB','Firebird','Db2','Informix','Sybase') +$databases = @('SQLite','SQLServer','PostgreSQL','Oracle','MySQL','MariaDB','Firebird','Db2','Informix','Sybase','Hana') $filter = if ($Database -eq 'Unit') { ($databases | ForEach-Object { "TestCategory!=$_" }) -join '&' } else { "TestCategory=$Database" } $xmlDirectory = Join-Path (Get-Location) "TestResults/$Database" dotnet test Migrator.slnx --no-build --filter $filter --logger "trx;LogFileName=$Database.trx" --results-directory TestResults -- NUnit.NumberOfTestWorkers=0 "NUnit.TestOutputXml=$xmlDirectory" @@ -15,6 +15,6 @@ if ([int]$counters.failed -gt 0) { throw "Failures in $Database results" } $skipped = @($results.TestRun.Results.UnitTestResult | Where-Object outcome -eq NotExecuted) Write-Host "$Database : $($counters.passed) passed, $($skipped.Count) skipped" foreach ($test in $skipped) { Write-Host "Skipped: $($test.testName) $($test.Output.ErrorInfo.Message)" } -if ($Database -in @('MySQL','MariaDB','Firebird','Db2','Informix','Sybase') -and $skipped.Count -gt 0) { +if ($Database -in @('MySQL','MariaDB','Firebird','Db2','Informix','Sybase','Hana') -and $skipped.Count -gt 0) { throw "New database suites must not skip tests." } diff --git a/.github/scripts/verify-test-coverage.py b/.github/scripts/verify-test-coverage.py index 8ef02980..002446e0 100644 --- a/.github/scripts/verify-test-coverage.py +++ b/.github/scripts/verify-test-coverage.py @@ -4,7 +4,7 @@ import xml.etree.ElementTree as ET expected = {"Unit", "SQLite", "SQLServer", "PostgreSQL", "Oracle", "MySQL", - "MariaDB", "Firebird", "Db2", "Informix", "Sybase"} + "MariaDB", "Firebird", "Db2", "Informix", "Sybase", "Hana"} seen = {} executed = 0 counts = set() diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 52020f5b..75969306 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - database: [Unit, SQLite, SQLServer, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase] + database: [Unit, SQLite, SQLServer, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase, Hana] steps: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/hana-qualification.yml b/.github/workflows/hana-qualification.yml deleted file mode 100644 index cbdc6b7a..00000000 --- a/.github/workflows/hana-qualification.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: SAP HANA qualification -on: - pull_request: - branches: [master, "codex/**"] - paths: - - ".github/workflows/hana-qualification.yml" - - ".github/qualification/Hana/**" - workflow_dispatch: -permissions: - contents: read -concurrency: - group: hana-qualification-${{ github.ref }} - cancel-in-progress: true -jobs: - hana: - name: Qualify actual SAP HANA engine and .NET driver - runs-on: ubuntu-22.04 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - name: Start disposable HANA Express - timeout-minutes: 22 - shell: bash - run: | - set -euo pipefail - sudo sysctl -w fs.file-max=20000000 fs.aio-max-nr=262144 vm.memory_failure_early_kill=1 vm.max_map_count=135217728 - mkdir -p "$RUNNER_TEMP/hana" - printf '{"master_password":"MgT9ci7Q4xZ2"}' > "$RUNNER_TEMP/hana/password.json" - sudo chown -R 12000:79 "$RUNNER_TEMP/hana" - sudo chmod 600 "$RUNNER_TEMP/hana/password.json" - docker pull saplabs/hanaexpress:2.00.088.00.20251110.1 - docker run -d --name migrator-hana --hostname hxe -p 39041:39041 \ - --ulimit nofile=1048576:1048576 \ - --sysctl kernel.shmmax=1073741824 --sysctl kernel.shmmni=32768 --sysctl kernel.shmall=8388608 \ - --sysctl net.ipv4.ip_local_port_range="40000 60999" \ - -v "$RUNNER_TEMP/hana:/hana/mounts" \ - saplabs/hanaexpress:2.00.088.00.20251110.1 \ - --passwords-url file:///hana/mounts/password.json --agree-to-sap-license - for attempt in $(seq 1 180); do - if docker exec migrator-hana /usr/sap/HXE/HDB90/exe/hdbsql -i 90 -d HXE -u SYSTEM -p MgT9ci7Q4xZ2 'SELECT 1 FROM DUMMY' >/dev/null 2>&1; then - exit 0 - fi - if [ "$(docker inspect -f '{{.State.Running}}' migrator-hana)" != true ]; then - docker logs migrator-hana - exit 1 - fi - sleep 5 - done - docker logs migrator-hana - exit 1 - - name: Test actual engine through SAP .NET driver - env: - MIGRATOR_HANA: "Server=localhost:39041;UserID=SYSTEM;Password=MgT9ci7Q4xZ2" - run: dotnet run --project .github/qualification/Hana/Hana.csproj - - name: Collect evidence - if: always() - run: | - mkdir -p TestResults - docker logs migrator-hana > TestResults/hana.log 2>&1 || true - free -m > TestResults/memory.txt - df -h > TestResults/disk.txt - - uses: actions/upload-artifact@v4 - if: always() - with: - name: hana-qualification - path: TestResults/ - - name: Cleanup - if: always() - run: docker rm -fv migrator-hana || true diff --git a/docs/additional-database-qualification.md b/docs/additional-database-qualification.md index 56bae41b..577e3664 100644 --- a/docs/additional-database-qualification.md +++ b/docs/additional-database-qualification.md @@ -7,7 +7,7 @@ This is an implementation gate, not a claim of released provider support. | Additional engine | Real-engine GitHub Actions route | Current disposition | | --- | --- | --- | -| SAP HANA | Official HANA Express Linux container and SAP's .NET driver, disposable schema | Qualification workflow added; provider admission requires a successful actual-engine run, followed by provider behavioral tests | +| SAP HANA | Official HANA Express Linux container and SAP's .NET driver, disposable schema | Infrastructure passed; provider and mandatory matrix tests added, pending their actual-engine results | | Amazon Redshift | AWS test warehouse/serverless endpoint with CI credentials, network access and resource cleanup | No configured test infrastructure; defer provider | | Snowflake | Snowflake test account, warehouse, credentials and disposable database/schema | No configured test infrastructure; defer provider | | Db2 for IBM i | IBM i endpoint on Power infrastructure and compatible .NET/ODBC driver | No configured test infrastructure; defer provider | @@ -23,14 +23,14 @@ not additional database engines. ## HANA admission criteria -The separate qualification workflow pins HANA Express 2.00.088.00.20251110.1 and +The HANA matrix startup pins HANA Express 2.00.088.00.20251110.1 and Sap.Data.Hana.Net.v8.0 2.30.27. It uses a disposable public CI credential, bounded startup, native .NET connection, identity/constraint DDL, persisted data, rollback and catalog checks. Any startup or behavioral failure fails the job. Logs and runner resource evidence are retained; cleanup runs independently of test success. -This is a prerequisite probe, not provider integration coverage. +The prerequisite probe passed in [run 35766200488](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35766200488). The standalone workflow is replaced by the mandatory Hana job in the complete database matrix; the probe project remains reproducible evidence. -If qualification passes, add the provider and mandatory matrix coverage for +Provider admission now requires the new matrix coverage for imperative/fluent schema creation, constraint metadata, data, migration history, restart/rollback, preview parity and explicit unsupported operations. Do not mark a provider supported on the basis of SQL string tests or a skipped secret-gated job. @@ -43,3 +43,27 @@ a provider supported on the basis of SQL string tests or a skipped secret-gated - [Snowflake local testing framework scope](https://docs.snowflake.com/en/developer-guide/snowpark/python/testing-locally) - [Db2 for IBM i platform](https://www.ibm.com/support/pages/db2-ibm-i) - [FluentMigrator's current provider FAQ](https://fluentmigrator.github.io/intro/faq.html) + +## HANA provider scope + +The source provider is selected by `ProviderTypes.Hana` and accepts a caller-owned +`HanaConnection` or the SAP factory. The core remains free of SAP driver references. +The tests use Sap.Data.Hana.Net.v8.0 2.30.27 and HANA Express 2.00.088.00.20251110.1. + +Supported operations include row/column table creation, explicit keys/checks/FKs, +column add/change/remove/rename, table rename/remove, ordinary and unique indexes, +parameterized data operations, schema/constraint/index metadata, versioned history, +and structured create-table/add-column preview. Names are case-preserving and accept +unquoted table or schema.table input; embedded identifier dots require explicit SQL. + +Whole-session transactions and native locking are not advertised. The provider +retains HANA's default DDL autocommit behavior; data rollback does not prove DDL rollback. +Use explicit SQL for tenant administration, computed columns, specialized indexes, +collation configuration, and provider-specific data types without a mapped CLR type. +The provider rejects unsupported included/filtered/clustered indexes and semantic +collation requests instead of ignoring them. + +[HANA constraints](https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/209f7cf5751910149d9ce6b033d8ddce.html), +[referential constraints](https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/20ccc0a175191014901b88e6bc175c44.html), +and [DDL autocommit](https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/d538d11053bd4f3f847ec5ce817a3d4c.html) +are documented by SAP. diff --git a/src/Migrator.Tests/Migrator.Tests.csproj b/src/Migrator.Tests/Migrator.Tests.csproj index 061f4d0f..912afe26 100644 --- a/src/Migrator.Tests/Migrator.Tests.csproj +++ b/src/Migrator.Tests/Migrator.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs b/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs new file mode 100644 index 00000000..2002b189 --- /dev/null +++ b/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.Fluent; +using DotNetProjects.Migrator.Providers; +using NUnit.Framework; +using Sap.Data.Hana; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace Migrator.Tests.Providers.Hana; + +[TestFixture, Category("Hana"), NonParallelizable] +public class HanaProviderTests +{ + private HanaConnection connection; + private ITransformationProvider provider; + private string schema; + [SetUp] + public void SetUp() + { + connection = new HanaConnection(Environment.GetEnvironmentVariable("MIGRATOR_HANA") + ?? "Server=localhost:39041;UserID=SYSTEM;Password=MgT9ci7Q4xZ2"); + connection.Open(); + schema = "MIGRATOR_" + Guid.NewGuid().ToString("N").ToUpperInvariant(); + using var command = connection.CreateCommand(); + command.CommandText = "CREATE SCHEMA " + schema; command.ExecuteNonQuery(); + command.CommandText = "SET SCHEMA " + schema; command.ExecuteNonQuery(); + provider = ProviderFactory.Create(ProviderTypes.Hana, connection, schema, "hana-tests"); + } + [TearDown] + public void TearDown() + { + provider?.Dispose(); + if (connection?.State == ConnectionState.Open && schema != null) + { + using var command = connection.CreateCommand(); command.CommandText = "DROP SCHEMA " + schema + " CASCADE"; command.ExecuteNonQuery(); + } + connection?.Dispose(); + } + [Test] + public void ConnectionStringFactoryOpensAndDisposesOwnedConnection() + { + using var owned = ProviderFactory.Create(ProviderTypes.Hana, connection.ConnectionString, schema); + Assert.That(Convert.ToInt32(owned.ExecuteScalar("SELECT 1 FROM DUMMY")), Is.EqualTo(1)); + } + + [Test] + public void ImperativeSchemaConstraintsMetadataAndPersistedData() + { + provider.AddTable("Items", new Column("Id", DbType.Int32) { IsIdentity = true }, + new Column("Label", DbType.String, 40) { DefaultValue = "initial" }, + new PrimaryKeyConstraint("PK_Items", "Id"), new UniqueConstraint("UQ_Label", "Label"), + new CheckConstraint("CK_Label", "LENGTH(\"Label\") > 0")); + provider.Insert("Items", ["Label"], ["one"]); + Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT COUNT(*) FROM \"Items\"")), Is.EqualTo(1)); + Assert.That(provider.GetColumns("Items").Single(c => c.Name == "Id").IsIdentity, Is.True); + var constraints = provider.GetTableConstraints("Items"); + Assert.That(constraints.OfType().Single().KeyColumns, Is.EqualTo(new[] { "Id" })); + Assert.That(constraints.OfType().Single().Name, Is.EqualTo("UQ_Label")); + Assert.That(constraints.OfType().Single().Name, Is.EqualTo("CK_Label")); + Assert.Catch(() => provider.Insert("Items", ["Label"], ["one"])); + Assert.Catch(() => provider.Insert("Items", ["Label"], [""])); + Assert.That(provider.GetColumns(schema + ".Items").Select(c => c.Name), Is.EqualTo(new[] { "Id", "Label" })); + } + [Test] + public void FluentAndPreviewCreateEquivalentSchemasAndRawDefaults() + { + MigrationBuilder Definition(string table) + { + var builder = new MigrationBuilder(); + builder.Create.Table(table).WithColumn("Id").AsInt32() + .WithColumn("Name").AsString(40).WithDefaultValue(RawSql.Insert("LOWER('ABC')")) + .WithPrimaryKey("PK_" + table, "Id"); + return builder; + } + Definition("Fluent").Apply(provider); + foreach (var sql in Definition("Preview").Preview(new SqlGenerationContext(ProviderTypes.Hana))) provider.ExecuteNonQuery(sql.TrimEnd(';')); + foreach (var table in new[] { "Fluent", "Preview" }) + { + provider.Insert(table, ["Id"], [1]); + Assert.That(provider.ExecuteScalar("SELECT \"Name\" FROM \"" + table + "\""), Is.EqualTo("abc")); + Assert.That(provider.GetColumns(table).Single(c => c.Name == "Name").DefaultValue, Is.TypeOf()); + Assert.That(provider.GetTableConstraints(table).OfType().Single().KeyColumns, Is.EqualTo(new[] { "Id" })); + } + } + [Test] + public void AlterRenameAndIndexOperationsPreserveData() + { + provider.AddTable("Names", new Column("Id", DbType.Int32), new Column("Label", DbType.String, 20)); + provider.Insert("Names", ["Id", "Label"], [1, "kept"]); + provider.AddColumn("Names", new Column("Extra", DbType.Int32) { DefaultValue = 7 }); + provider.ChangeColumn("Names", new Column("Label", DbType.String, 60)); + provider.RenameColumn("Names", "Label", "Text"); + provider.RenameTable("Names", "Renamed"); + provider.AddIndex("Renamed", new Index { Name = "IX_Text", KeyColumns = ["Text"] }); + Assert.That(provider.IndexExists("Renamed", "IX_Text"), Is.True); + Assert.That(provider.GetIndexes("Renamed").Single(i => i.Name == "IX_Text").KeyColumns, Is.EqualTo(new[] { "Text" })); + Assert.That(provider.ExecuteScalar("SELECT \"Text\" FROM \"Renamed\""), Is.EqualTo("kept")); + provider.RemoveIndex("Renamed", "IX_Text"); + provider.RemoveColumn("Renamed", "Extra"); + Assert.That(provider.ColumnExists("Renamed", "Extra"), Is.False); + provider.RemoveTable("Renamed"); + Assert.That(provider.TableExists("Renamed"), Is.False); + } + [Test] + public void ForeignKeysPreservePairsAndIndependentActions() + { + provider.AddTable("Parents", new Column("A", DbType.Int32), new Column("B", DbType.Int32), + new PrimaryKeyConstraint("PK_Parents", "B", "A")); + provider.AddTable("Children", new Column("X", DbType.Int32), new Column("Y", DbType.Int32)); + ((IForeignKeyActions)provider).AddForeignKey("FK_Children", "Children", ["X", "Y"], "Parents", ["B", "A"], + ForeignKeyConstraintType.Cascade, ForeignKeyConstraintType.Restrict); + var key = provider.GetForeignKeyConstraints("Children").Single(); + Assert.That(key.ParentColumns, Is.EqualTo(new[] { "B", "A" })); + Assert.That(key.ChildColumns, Is.EqualTo(new[] { "X", "Y" })); + Assert.That(key.OnDelete, Is.EqualTo("CASCADE")); + Assert.That(key.OnUpdate, Is.EqualTo("RESTRICT")); + provider.Insert("Parents", ["A", "B"], [1, 2]); + provider.Insert("Children", ["X", "Y"], [2, 1]); + provider.Delete("Parents", ["A"], [1]); + Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT COUNT(*) FROM \"Children\"")), Is.Zero); + provider.RemoveForeignKey("Children", "FK_Children"); + Assert.That(provider.GetForeignKeyConstraints("Children"), Is.Empty); + } + [Test] + public void DataTransactionsRollbackAndCallerConnectionSurvives() + { + provider.AddTable("Numbers", new Column("Id", DbType.Int32)); + provider.Insert("Numbers", ["Id"], [1]); + provider.BeginTransaction(); + provider.Insert("Numbers", ["Id"], [2]); + provider.Rollback(); + Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT COUNT(*) FROM \"Numbers\"")), Is.EqualTo(1)); + provider.Dispose(); + Assert.That(connection.State, Is.EqualTo(ConnectionState.Open)); + } + [Test] + public void RunnerHistoryRestartDowngradeAndReadonlyPlan() + { + var runner = new DotNetProjects.Migrator.Migrator(provider, false, typeof(HanaMigration)); + runner.DryRun = true; + runner.MigrateToLastVersion(); + Assert.That(provider.TableExists(provider.SchemaInfoTable), Is.False); + runner.DryRun = false; + runner.MigrateToLastVersion(); + Assert.That(provider.TableExists("RunnerItems"), Is.True); + var restarted = new DotNetProjects.Migrator.Migrator(provider, false, typeof(HanaMigration)); + restarted.MigrateToLastVersion(); + Assert.That(((IMigrationHistory)provider).ReadAppliedMigrations(), Is.EqualTo(new long[] { 1 })); + restarted.MigrateTo(0); + Assert.That(provider.TableExists("RunnerItems"), Is.False); + Assert.That(((IMigrationHistory)provider).ReadAppliedMigrations(), Is.Empty); + } + [Test] + public void UnsupportedCapabilitiesFailBeforeSchemaChanges() + { + Assert.Throws(() => provider.AddTable("InvalidCollation", + new Column("Name", DbType.String, 40) { Collation = Collation.CaseInsensitive })); + Assert.That(provider.TableExists("InvalidCollation"), Is.False); + Assert.Throws(() => provider.CreateDatabases("unused")); + var runner = new DotNetProjects.Migrator.Migrator(provider, false, typeof(HanaMigration)); + runner.Options.TransactionMode = MigrationTransactionMode.WholeSession; + Assert.Catch(() => runner.MigrateToLastVersion()); + Assert.That(provider.TableExists("RunnerItems"), Is.False); + } + [Migration(1, Scope = "hana-tests", Ignore = true)] + public class HanaMigration : Migration + { + public override void Up() => Database.AddTable("RunnerItems", new Column("Id", DbType.Int32), new PrimaryKeyConstraint("PK_RunnerItems", "Id")); + public override void Down() => Database.RemoveTable("RunnerItems"); + } +} diff --git a/src/Migrator/Framework/Fluent/Operations.cs b/src/Migrator/Framework/Fluent/Operations.cs index f5d1d67e..bfca7461 100644 --- a/src/Migrator/Framework/Fluent/Operations.cs +++ b/src/Migrator/Framework/Fluent/Operations.cs @@ -50,7 +50,7 @@ public override string ToSql(SqlGenerationContext c) var definitions = columns.Select(c.Column).ToList(); definitions.AddRange(Fields.OfType().Select(c.Dialect.GetTableConstraintSql)); c.AddTable(Table, columns); - return $"CREATE TABLE {c.Table(Table)} ({string.Join(", ", definitions)});"; + return $"CREATE {(c.Provider == ProviderTypes.Hana ? "ROW " : "")}TABLE {c.Table(Table)} ({string.Join(", ", definitions)});"; } } public sealed record ColumnOperation(string Table, Column Column, bool Alter = false) : MigrationOperation @@ -62,7 +62,7 @@ public override string ToSql(SqlGenerationContext c) c.RequireTable(Table); if (Alter) throw new NotSupportedException("Altering columns needs provider-specific schema inspection; use explicit SQL preview."); c.AddColumn(Table, Column); - return $"ALTER TABLE {c.Table(Table)} ADD {c.Column(Column)};"; + return c.Provider == ProviderTypes.Hana ? $"ALTER TABLE {c.Table(Table)} ADD ({c.Column(Column)});" : $"ALTER TABLE {c.Table(Table)} ADD {c.Column(Column)};"; } } public enum RemoveKind { Table, Column, ForeignKey, Constraint, PrimaryKey, Default, Index, AllIndexes, AllConstraints, ForeignKeysForColumn, Truncate } diff --git a/src/Migrator/ProviderFactory.cs b/src/Migrator/ProviderFactory.cs index acb1fc2b..df7f7931 100644 --- a/src/Migrator/ProviderFactory.cs +++ b/src/Migrator/ProviderFactory.cs @@ -50,6 +50,8 @@ public static Dialect DialectForProvider(ProviderTypes providerType) { switch (providerType) { + case ProviderTypes.Hana: + return new DotNetProjects.Migrator.Providers.Impl.Hana.HanaDialect(); case ProviderTypes.SQLite: return (Dialect)Activator.CreateInstance(typeof(SQLiteDialect)); case ProviderTypes.MonoSQLite: diff --git a/src/Migrator/Providers/Impl/Hana/HanaDialect.cs b/src/Migrator/Providers/Impl/Hana/HanaDialect.cs new file mode 100644 index 00000000..1f7cfbf4 --- /dev/null +++ b/src/Migrator/Providers/Impl/Hana/HanaDialect.cs @@ -0,0 +1,58 @@ +using System; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; + +namespace DotNetProjects.Migrator.Providers.Impl.Hana; + +public class HanaDialect : Dialect +{ + public HanaDialect() + { + RegisterColumnType(DbType.Int16, "SMALLINT"); + RegisterColumnType(DbType.Int32, "INTEGER"); + RegisterColumnType(DbType.Int64, "BIGINT"); + RegisterColumnType(DbType.Byte, "TINYINT"); + RegisterColumnType(DbType.Boolean, "BOOLEAN"); + RegisterColumnType(DbType.Decimal, "DECIMAL(18, 4)"); + RegisterColumnType(DbType.Currency, "DECIMAL(18, 4)"); + RegisterColumnTypeWithParameters(DbType.Decimal, "DECIMAL({precision},{scale})"); + RegisterColumnType(DbType.Double, "DOUBLE"); + RegisterColumnType(DbType.Single, "REAL"); + RegisterColumnType(DbType.String, "NVARCHAR(255)"); + RegisterColumnType(DbType.String, 5000, "NVARCHAR($l)"); + RegisterColumnType(DbType.String, int.MaxValue, "NCLOB"); + RegisterColumnType(DbType.AnsiString, "VARCHAR(255)"); + RegisterColumnType(DbType.AnsiString, 5000, "VARCHAR($l)"); + RegisterColumnType(DbType.AnsiString, int.MaxValue, "CLOB"); + RegisterColumnType(DbType.StringFixedLength, "NCHAR(255)"); + RegisterColumnType(DbType.StringFixedLength, 5000, "NCHAR($l)"); + RegisterColumnType(DbType.AnsiStringFixedLength, "CHAR(255)"); + RegisterColumnType(DbType.AnsiStringFixedLength, 5000, "CHAR($l)"); + RegisterColumnType(DbType.Binary, "BLOB"); + RegisterColumnType(DbType.Binary, 5000, "VARBINARY($l)"); + RegisterColumnType(DbType.Date, "DATE"); + RegisterColumnType(DbType.Time, "TIME"); + RegisterColumnType(DbType.DateTime, "TIMESTAMP"); + RegisterColumnType(DbType.DateTime2, "TIMESTAMP"); + RegisterColumnAttribute(ColumnAttribute.Identity, "GENERATED BY DEFAULT AS IDENTITY"); + } + public override bool TableNameNeedsQuote => true; + public override bool ColumnNameNeedsQuote => true; + public override bool ConstraintNameNeedsQuote => true; + public override bool IdentityNeedsType => true; + public override bool SupportsIndex => false; + public override string QuoteTemplate => "\"{0}\""; + public override string Quote(string name) => string.Join(".", name.Split('.').Select(QuoteIdentifier)); + public override string Default(object value) => value switch + { + bool boolean => boolean ? "DEFAULT TRUE" : "DEFAULT FALSE", + TimeSpan time when time >= TimeSpan.Zero && time < TimeSpan.FromDays(1) => "DEFAULT '" + time.ToString("c", System.Globalization.CultureInfo.InvariantCulture) + "'", + byte[] bytes => "DEFAULT X'" + Convert.ToHexString(bytes) + "'", + _ => base.Default(value) + }; + public override ITransformationProvider GetTransformationProvider(Dialect dialect, string connectionString, string defaultSchema, string scope, string providerName) => + new HanaTransformationProvider(dialect, connectionString, defaultSchema, scope, providerName); + public override ITransformationProvider GetTransformationProvider(Dialect dialect, IDbConnection connection, string defaultSchema, string scope, string providerName) => + new HanaTransformationProvider(dialect, connection, defaultSchema, scope); +} diff --git a/src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs b/src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs new file mode 100644 index 00000000..d03809f7 --- /dev/null +++ b/src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using Index = DotNetProjects.Migrator.Framework.Index; +using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; +using UniqueConstraint = DotNetProjects.Migrator.Framework.UniqueConstraint; + +namespace DotNetProjects.Migrator.Providers.Impl.Hana; + +/// SAP HANA 2 provider. DDL uses the engine's default autocommit behavior; +/// whole-session transactional DDL and native migration locks are not advertised. +public class HanaTransformationProvider : TransformationProvider +{ + public HanaTransformationProvider(Dialect dialect, string connectionString, string defaultSchema, string scope, string providerName) + : base(dialect, connectionString, defaultSchema, scope) + { + var factory = DbProviderFactoriesHelper.GetFactory(string.IsNullOrEmpty(providerName) ? "Sap.Data.Hana" : providerName, + "Sap.Data.Hana.Net.v8.0", "Sap.Data.Hana.HanaFactory"); + _connection = factory.CreateConnection(); + _connection.ConnectionString = connectionString; + _connection.Open(); + } + public HanaTransformationProvider(Dialect dialect, IDbConnection connection, string defaultSchema, string scope) + : base(dialect, connection, defaultSchema, scope) { } + + // SAP's ADO.NET driver uses positional parameters. + public override string GenerateParameterName(int index) => "?"; + public override string GenerateParameterNameParameter(int index) => "p" + index; + private (string Schema, string Table) Name(string table) + { + var parts = table.Split('.'); + if (parts.Length > 2 || parts.Any(string.IsNullOrWhiteSpace) || parts.Any(x => x.Contains('"'))) + throw new NotSupportedException("HANA names must be unquoted table or schema.table names. Embedded dots/quotes require explicit SQL."); + return (parts.Length == 2 ? parts[0] : _defaultSchema, parts[^1]); + } + public override string QuoteTableNameIfRequired(string table) + { + var name = Name(table); + return (name.Schema == null ? "" : Dialect.QuoteIdentifier(name.Schema) + ".") + Dialect.QuoteIdentifier(name.Table); + } + public override string QuoteColumnNameIfRequired(string column) => Dialect.QuoteIdentifier(column); + private IDbCommand Catalog(string sql, params object[] values) + { + var command = CreateCommand(); + command.CommandText = sql; + for (var i = 0; i < values.Length; i++) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = "p" + i; parameter.DbType = DbType.String; + parameter.Value = values[i] ?? DBNull.Value; command.Parameters.Add(parameter); + } + return command; + } + private bool Exists(string view, string table) + { + var name = Name(table); + using var command = Catalog("SELECT COUNT(*) FROM SYS." + view + " WHERE SCHEMA_NAME=COALESCE(?,CURRENT_SCHEMA) AND " + (view == "VIEWS" ? "VIEW_NAME" : "TABLE_NAME") + "=?", name.Schema, name.Table); + return Convert.ToInt32(command.ExecuteScalar()) > 0; + } + public override bool TableExists(string table) => Exists("TABLES", table); + public override bool ViewExists(string table) => Exists("VIEWS", table); + public override string[] GetTables() + { + using var command = Catalog("SELECT TABLE_NAME FROM SYS.TABLES WHERE SCHEMA_NAME=COALESCE(?,CURRENT_SCHEMA) ORDER BY TABLE_NAME", _defaultSchema); + using var reader = command.ExecuteReader(); + var names = new List(); while (reader.Read()) names.Add(reader.GetString(0)); return names.ToArray(); + } + public override List GetDatabases() => [Convert.ToString(ExecuteScalar("SELECT DATABASE_NAME FROM SYS.M_DATABASE"))]; + public override void SwitchDatabase(string databaseName) => throw new NotSupportedException("Connect to the target HANA tenant explicitly."); + public override void CreateDatabases(string databaseName) => throw new NotSupportedException("HANA tenant administration requires an explicit SYSTEMDB connection and operation."); + public override void DropDatabases(string databaseName) => throw new NotSupportedException("HANA tenant administration requires an explicit SYSTEMDB connection and operation."); + public override void KillDatabaseConnections(string databaseName) => throw new NotSupportedException("Use explicit HANA connection administration."); + + public override void AddTable(string table, string engine, string columns) + { + if (engine != null && engine is not ("ROW" or "COLUMN")) throw new NotSupportedException("HANA table engine must be ROW or COLUMN."); + ExecuteNonQuery($"CREATE {engine ?? "ROW"} TABLE {QuoteTableNameIfRequired(table)} ({columns})"); + } + public override void AddColumn(string table, string definition) => ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ADD ({definition})"); + public override void ChangeColumn(string table, string definition) => ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ALTER ({definition})"); + public override void ChangeColumn(string table, Column column) + { + if (column.IsIdentity) throw new NotSupportedException("Changing HANA identity properties requires explicit SQL."); + ChangeColumn(table, _dialect.GetAndMapColumnProperties(column.CopyDefinition()).ColumnSql); + } + public override void RemoveColumn(string table, string column) => ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} DROP ({QuoteColumnNameIfRequired(column)})"); + public override void RemoveTable(string table) => ExecuteNonQuery("DROP TABLE " + QuoteTableNameIfRequired(table)); + public override void RenameTable(string table, string name) => ExecuteNonQuery($"RENAME TABLE {QuoteTableNameIfRequired(table)} TO {QuoteTableNameIfRequired(name)}"); + public override void RenameColumn(string table, string column, string name) => ExecuteNonQuery($"RENAME COLUMN {QuoteTableNameIfRequired(table)}.{QuoteColumnNameIfRequired(column)} TO {QuoteColumnNameIfRequired(name)}"); + public override void RemoveColumnDefaultValue(string table, string column) => + ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ALTER ({QuoteColumnNameIfRequired(column)} DROP DEFAULT)"); + public override void AddColumnDefaultValue(string table, string column, object value) => + ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ALTER ({QuoteColumnNameIfRequired(column)} SET {Dialect.Default(value)})"); + public override int TruncateTable(string table) => ExecuteNonQuery("TRUNCATE TABLE " + QuoteTableNameIfRequired(table)); + public override void AddForeignKey(string name, string child, string[] columns, string parent, string[] parentColumns, ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) => + base.AddForeignKey(name, child, columns, parent, parentColumns, + onDelete == ForeignKeyConstraintType.NoAction ? ForeignKeyConstraintType.Restrict : onDelete, + onUpdate == ForeignKeyConstraintType.NoAction ? ForeignKeyConstraintType.Restrict : onUpdate); + public override string[] GetConstraints(string table) => GetTableConstraints(table).Select(c => c.Name).ToArray(); + public override bool ConstraintExists(string table, string name) => GetConstraints(table).Contains(name, StringComparer.Ordinal); + protected override string GetPrimaryKeyConstraintName(string table) => GetTableConstraints(table).OfType().SingleOrDefault()?.Name; + public override bool PrimaryKeyExists(string table, string name) => GetPrimaryKeyConstraintName(table) is string actual && actual == name; + public override void RemoveAllForeignKeys(string table, string column) + { + foreach (var key in GetForeignKeyConstraints(table).Where(k => k.ChildColumns.Contains(column))) RemoveForeignKey(table, key.Name); + } + public override Column[] GetColumns(string table) + { + var name = Name(table); + using var command = Catalog("SELECT COLUMN_NAME,DATA_TYPE_NAME,LENGTH,SCALE,IS_NULLABLE,DEFAULT_VALUE,GENERATION_TYPE FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME=COALESCE(?,CURRENT_SCHEMA) AND TABLE_NAME=? ORDER BY POSITION", name.Schema, name.Table); + using var reader = command.ExecuteReader(); var columns = new List(); + while (reader.Read()) + { + var typeName = reader.GetString(1); + var type = typeName switch + { + "TINYINT" => DbType.Byte, "SMALLINT" => DbType.Int16, "INTEGER" => DbType.Int32, "BIGINT" => DbType.Int64, + "BOOLEAN" => DbType.Boolean, "DECIMAL" => DbType.Decimal, "REAL" => DbType.Single, "DOUBLE" => DbType.Double, + "VARCHAR" or "CLOB" => DbType.AnsiString, "NVARCHAR" or "NCLOB" or "SHORTTEXT" => DbType.String, + "CHAR" => DbType.AnsiStringFixedLength, "NCHAR" => DbType.StringFixedLength, + "BINARY" or "VARBINARY" or "BLOB" => DbType.Binary, + "DATE" => DbType.Date, "TIME" => DbType.Time, "TIMESTAMP" or "SECONDDATE" => DbType.DateTime, + _ => throw new NotSupportedException("HANA catalog type is not representable: " + typeName) + }; + var generation = reader.IsDBNull(6) ? null : reader.GetString(6); + if (!string.IsNullOrEmpty(generation) && !generation.Contains("IDENTITY")) throw new NotSupportedException("HANA computed column metadata requires explicit SQL."); + var column = new Column(reader.GetString(0), type) { IsNullable = reader.GetString(4) == "TRUE", IsIdentity = generation?.Contains("IDENTITY") == true }; + if (type == DbType.Decimal) { column.Precision = Convert.ToInt32(reader.GetValue(2)); column.Scale = Convert.ToInt32(reader.GetValue(3)); } + else if (typeName is "NCLOB" or "CLOB") column.Size = int.MaxValue; + else if (type is DbType.String or DbType.AnsiString or DbType.StringFixedLength or DbType.AnsiStringFixedLength || typeName == "VARBINARY") column.Size = Convert.ToInt32(reader.GetValue(2)); + if (!reader.IsDBNull(5) && !column.IsIdentity) column.DefaultValue = CatalogDefaultValue.Parse(reader.GetString(5), type); + columns.Add(column); + } + return columns.ToArray(); + } + public override TableConstraint[] GetTableConstraints(string table) + { + var name = Name(table); + var rows = new List<(string Name, string Column, bool Primary, bool Unique, string Check)>(); + using (var command = Catalog("SELECT CONSTRAINT_NAME,COLUMN_NAME,IS_PRIMARY_KEY,IS_UNIQUE_KEY,CHECK_CONDITION FROM SYS.CONSTRAINTS WHERE SCHEMA_NAME=COALESCE(?,CURRENT_SCHEMA) AND TABLE_NAME=? ORDER BY CONSTRAINT_NAME,POSITION", name.Schema, name.Table)) + using (var reader = command.ExecuteReader()) + while (reader.Read()) rows.Add((reader.GetString(0), reader.IsDBNull(1) ? null : reader.GetString(1), !reader.IsDBNull(2) && reader.GetString(2) == "TRUE", !reader.IsDBNull(3) && reader.GetString(3) == "TRUE", reader.IsDBNull(4) ? null : reader.GetString(4))); + var constraints = rows.GroupBy(r => r.Name).Select(g => g.First().Primary ? (TableConstraint)new PrimaryKeyConstraint(g.Key, g.Select(r => r.Column).ToArray()) + : g.First().Unique ? new UniqueConstraint(g.Key, g.Select(r => r.Column).ToArray()) + : g.First().Check != null ? new CheckConstraint(g.Key, g.First().Check) + : throw new NotSupportedException("Unsupported HANA constraint: " + g.Key)).ToList(); + constraints.AddRange(GetForeignKeyConstraints(table)); return constraints.ToArray(); + } + public override ForeignKeyConstraint[] GetForeignKeyConstraints(string table) + { + var name = Name(table); + var rows = new List<(string Name, string Column, string ParentSchema, string Parent, string ParentColumn, string Delete, string Update)>(); + using (var command = Catalog("SELECT CONSTRAINT_NAME,COLUMN_NAME,REFERENCED_SCHEMA_NAME,REFERENCED_TABLE_NAME,REFERENCED_COLUMN_NAME,DELETE_RULE,UPDATE_RULE FROM SYS.REFERENTIAL_CONSTRAINTS WHERE SCHEMA_NAME=COALESCE(?,CURRENT_SCHEMA) AND TABLE_NAME=? ORDER BY CONSTRAINT_NAME,POSITION", name.Schema, name.Table)) + using (var reader = command.ExecuteReader()) + while (reader.Read()) rows.Add((reader.GetString(0), reader.GetString(1), reader.GetString(2), reader.GetString(3), reader.GetString(4), reader.GetString(5), reader.GetString(6))); + return rows.GroupBy(r => r.Name).Select(g => new ForeignKeyConstraint(g.Key, g.First().ParentSchema + "." + g.First().Parent, + g.Select(r => r.ParentColumn).ToArray(), table, g.Select(r => r.Column).ToArray()) + { OnDelete = g.First().Delete, OnUpdate = g.First().Update }).ToArray(); + } + public override string AddIndex(string table, Index index) + { + if (index.Clustered || index.IncludeColumns?.Length > 0 || index.FilterItems?.Count > 0) + throw new NotSupportedException("HANA index INCLUDE, clustered and filtered options are not supported by this provider."); + if (index.KeyColumns?.Length is not > 0) throw new ArgumentException("Index key columns are required.", nameof(index)); + var name = index.Name ?? "IX_" + Name(table).Table + "_" + string.Join("_", index.KeyColumns); + ExecuteNonQuery($"CREATE {(index.Unique ? "UNIQUE " : "")}INDEX {Dialect.QuoteIdentifier(name)} ON {QuoteTableNameIfRequired(table)} ({string.Join(", ", index.KeyColumns.Select(QuoteColumnNameIfRequired))})"); + return name; + } + public override Index[] GetIndexes(string table) + { + var name = Name(table); + var rows = new List<(string Name, string Column, string Constraint)>(); + using (var command = Catalog("SELECT INDEX_NAME,COLUMN_NAME,CONSTRAINT FROM SYS.INDEX_COLUMNS WHERE SCHEMA_NAME=COALESCE(?,CURRENT_SCHEMA) AND TABLE_NAME=? ORDER BY INDEX_NAME,POSITION", name.Schema, name.Table)) + using (var reader = command.ExecuteReader()) + while (reader.Read()) rows.Add((reader.GetString(0), reader.GetString(1), reader.IsDBNull(2) ? "" : reader.GetString(2))); + var constraints = GetTableConstraints(table).ToDictionary(c => c.Name, StringComparer.Ordinal); + return rows.GroupBy(r => r.Name).Select(g => new Index { Name = g.Key, KeyColumns = g.Select(r => r.Column).ToArray(), + Unique = g.First().Constraint.Contains("UNIQUE") || g.First().Constraint == "PRIMARY_KEY", + PrimaryKey = constraints.TryGetValue(g.Key, out var c) && c is PrimaryKeyConstraint, + UniqueConstraint = constraints.TryGetValue(g.Key, out var u) && u is UniqueConstraint }).ToArray(); + } + public override bool IndexExists(string table, string name) => GetIndexes(table).Any(i => i.Name == name); + public override void RemoveIndex(string table, string name) + { + var schema = Name(table).Schema; + ExecuteNonQuery("DROP INDEX " + (schema == null ? "" : Dialect.QuoteIdentifier(schema) + ".") + Dialect.QuoteIdentifier(name)); + } + public override void RemoveAllIndexes(string table) + { + foreach (var index in GetIndexes(table).Where(i => !i.PrimaryKey && !i.UniqueConstraint)) RemoveIndex(table, index.Name); + } +} diff --git a/src/Migrator/Providers/ProviderTypes.cs b/src/Migrator/Providers/ProviderTypes.cs index 7a7dbcba..8dd2f21c 100644 --- a/src/Migrator/Providers/ProviderTypes.cs +++ b/src/Migrator/Providers/ProviderTypes.cs @@ -18,4 +18,5 @@ public enum ProviderTypes Firebird, Ingres, Sybase, + Hana, } From fbe9c5833d54a2c2714fa8b4b5c00e581c06eb09 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:41:01 +0200 Subject: [PATCH 31/34] Fix HANA factory activation and use engine-supported column defaults The first mandatory HANA matrix run passed six of eight scenarios. Resolve the two failures: SAP exposes HanaFactory.Instance with a private constructor, and HANA does not permit LOWER(...) as a column default. Factory discovery now honors the standard public singleton field/property before constructor fallback. Exercise CURRENT_TIMESTAMP raw defaults through imperative, fluent and generated SQL paths, including catalog round trips and persisted timestamp values. Use HANA's complete ALTER column definition for default changes, replace removed defaults with DEFAULT NULL, and emit NULL when relaxing nullability. Extend the actual-engine alteration test to verify both changed/defaultless inserts and NOT NULL -> NULL transitions. This follows SAP's documented ALTER syntax. Validation: solution build and 87 unit tests pass locally. Existing actual-engine run 35767879821 identified the failures; all other database jobs passed. HANA changes require a fresh mandatory matrix run before provider admission. --- .../Providers/Hana/HanaProviderTests.cs | 22 ++++++++++++++----- .../Providers/DbProviderFactoriesHelper.cs | 10 ++++++++- .../Providers/Impl/Hana/HanaDialect.cs | 1 + .../Impl/Hana/HanaTransformationProvider.cs | 13 +++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs b/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs index 2002b189..00618c45 100644 --- a/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs +++ b/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs @@ -72,17 +72,20 @@ MigrationBuilder Definition(string table) { var builder = new MigrationBuilder(); builder.Create.Table(table).WithColumn("Id").AsInt32() - .WithColumn("Name").AsString(40).WithDefaultValue(RawSql.Insert("LOWER('ABC')")) + .WithColumn("Created").AsDateTime().WithDefaultValue(RawSql.Insert("CURRENT_TIMESTAMP")) .WithPrimaryKey("PK_" + table, "Id"); return builder; } + provider.AddTable("Imperative", new Column("Id", DbType.Int32), + new Column("Created", DbType.DateTime) { DefaultValue = RawSql.Insert("CURRENT_TIMESTAMP") }, + new PrimaryKeyConstraint("PK_Imperative", "Id")); Definition("Fluent").Apply(provider); foreach (var sql in Definition("Preview").Preview(new SqlGenerationContext(ProviderTypes.Hana))) provider.ExecuteNonQuery(sql.TrimEnd(';')); - foreach (var table in new[] { "Fluent", "Preview" }) + foreach (var table in new[] { "Imperative", "Fluent", "Preview" }) { provider.Insert(table, ["Id"], [1]); - Assert.That(provider.ExecuteScalar("SELECT \"Name\" FROM \"" + table + "\""), Is.EqualTo("abc")); - Assert.That(provider.GetColumns(table).Single(c => c.Name == "Name").DefaultValue, Is.TypeOf()); + Assert.That(provider.ExecuteScalar("SELECT \"Created\" FROM \"" + table + "\""), Is.TypeOf()); + Assert.That(provider.GetColumns(table).Single(c => c.Name == "Created").DefaultValue, Is.TypeOf()); Assert.That(provider.GetTableConstraints(table).OfType().Single().KeyColumns, Is.EqualTo(new[] { "Id" })); } } @@ -93,12 +96,21 @@ public void AlterRenameAndIndexOperationsPreserveData() provider.Insert("Names", ["Id", "Label"], [1, "kept"]); provider.AddColumn("Names", new Column("Extra", DbType.Int32) { DefaultValue = 7 }); provider.ChangeColumn("Names", new Column("Label", DbType.String, 60)); + ((TransformationProvider)provider).AddColumnDefaultValue("Names", "Extra", 8); + provider.Insert("Names", ["Id", "Label"], [2, "second"]); + Assert.That(Convert.ToInt32(provider.ExecuteScalar("SELECT \"Extra\" FROM \"Names\" WHERE \"Id\"=2")), Is.EqualTo(8)); + provider.RemoveColumnDefaultValue("Names", "Extra"); + provider.ChangeColumn("Names", new Column("Label", DbType.String, 60) { IsNullable = false }); + provider.ChangeColumn("Names", new Column("Label", DbType.String, 60) { IsNullable = true }); + provider.Insert("Names", ["Id"], [3]); + Assert.That(provider.ExecuteScalar("SELECT \"Extra\" FROM \"Names\" WHERE \"Id\"=3"), Is.EqualTo(DBNull.Value)); + Assert.That(provider.GetColumns("Names").Single(c => c.Name == "Label").IsNullable, Is.True); provider.RenameColumn("Names", "Label", "Text"); provider.RenameTable("Names", "Renamed"); provider.AddIndex("Renamed", new Index { Name = "IX_Text", KeyColumns = ["Text"] }); Assert.That(provider.IndexExists("Renamed", "IX_Text"), Is.True); Assert.That(provider.GetIndexes("Renamed").Single(i => i.Name == "IX_Text").KeyColumns, Is.EqualTo(new[] { "Text" })); - Assert.That(provider.ExecuteScalar("SELECT \"Text\" FROM \"Renamed\""), Is.EqualTo("kept")); + Assert.That(provider.ExecuteScalar("SELECT \"Text\" FROM \"Renamed\" WHERE \"Id\"=1"), Is.EqualTo("kept")); provider.RemoveIndex("Renamed", "IX_Text"); provider.RemoveColumn("Renamed", "Extra"); Assert.That(provider.ColumnExists("Renamed", "Extra"), Is.False); diff --git a/src/Migrator/Providers/DbProviderFactoriesHelper.cs b/src/Migrator/Providers/DbProviderFactoriesHelper.cs index 651d8f78..4fc110b1 100644 --- a/src/Migrator/Providers/DbProviderFactoriesHelper.cs +++ b/src/Migrator/Providers/DbProviderFactoriesHelper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data.Common; using System.Linq; +using System.Reflection; namespace DotNetProjects.Migrator.Providers; @@ -37,7 +38,14 @@ public static DbProviderFactory GetFactory(string providerName, string assemblyN #if NETSTANDARD return null; #else - return (DbProviderFactory)AppDomain.CurrentDomain.CreateInstanceAndUnwrap(assemblyName, factoryProviderType); + var type = Assembly.Load(assemblyName).GetType(factoryProviderType, throwOnError: true); + const BindingFlags flags = BindingFlags.Public | BindingFlags.Static; + // ADO.NET factories commonly expose a singleton and have a private constructor. + if (type.GetField("Instance", flags)?.GetValue(null) is DbProviderFactory fieldFactory) + return fieldFactory; + if (type.GetProperty("Instance", flags)?.GetValue(null) is DbProviderFactory propertyFactory) + return propertyFactory; + return (DbProviderFactory)Activator.CreateInstance(type); #endif } } diff --git a/src/Migrator/Providers/Impl/Hana/HanaDialect.cs b/src/Migrator/Providers/Impl/Hana/HanaDialect.cs index 1f7cfbf4..cf94b0aa 100644 --- a/src/Migrator/Providers/Impl/Hana/HanaDialect.cs +++ b/src/Migrator/Providers/Impl/Hana/HanaDialect.cs @@ -41,6 +41,7 @@ public HanaDialect() public override bool ColumnNameNeedsQuote => true; public override bool ConstraintNameNeedsQuote => true; public override bool IdentityNeedsType => true; + public override bool NeedsNullForNullableWhenAlteringTable => true; public override bool SupportsIndex => false; public override string QuoteTemplate => "\"{0}\""; public override string Quote(string name) => string.Join(".", name.Split('.').Select(QuoteIdentifier)); diff --git a/src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs b/src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs index d03809f7..7c534561 100644 --- a/src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Hana/HanaTransformationProvider.cs @@ -89,10 +89,15 @@ public override void ChangeColumn(string table, Column column) public override void RemoveTable(string table) => ExecuteNonQuery("DROP TABLE " + QuoteTableNameIfRequired(table)); public override void RenameTable(string table, string name) => ExecuteNonQuery($"RENAME TABLE {QuoteTableNameIfRequired(table)} TO {QuoteTableNameIfRequired(name)}"); public override void RenameColumn(string table, string column, string name) => ExecuteNonQuery($"RENAME COLUMN {QuoteTableNameIfRequired(table)}.{QuoteColumnNameIfRequired(column)} TO {QuoteColumnNameIfRequired(name)}"); - public override void RemoveColumnDefaultValue(string table, string column) => - ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ALTER ({QuoteColumnNameIfRequired(column)} DROP DEFAULT)"); - public override void AddColumnDefaultValue(string table, string column, object value) => - ExecuteNonQuery($"ALTER TABLE {QuoteTableNameIfRequired(table)} ALTER ({QuoteColumnNameIfRequired(column)} SET {Dialect.Default(value)})"); + public override void RemoveColumnDefaultValue(string table, string column) => AddColumnDefaultValue(table, column, RawSql.Insert("NULL")); + public override void AddColumnDefaultValue(string table, string column, object value) + { + var definition = GetColumns(table).SingleOrDefault(c => c.Name == column) + ?? throw new ArgumentException("HANA column does not exist: " + column, nameof(column)); + if (definition.IsIdentity) throw new NotSupportedException("HANA identity defaults cannot be changed."); + definition.DefaultValue = value ?? RawSql.Insert("NULL"); + ChangeColumn(table, definition); + } public override int TruncateTable(string table) => ExecuteNonQuery("TRUNCATE TABLE " + QuoteTableNameIfRequired(table)); public override void AddForeignKey(string name, string child, string[] columns, string parent, string[] parentColumns, ForeignKeyConstraintType onDelete, ForeignKeyConstraintType onUpdate) => base.AddForeignKey(name, child, columns, parent, parentColumns, From eabec5561c2c9c77847b9adbb7b67474e3c70a50 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:52:43 +0200 Subject: [PATCH 32/34] Retain the original CI connection string for HANA factory integration tests SAP removes credentials from HanaConnection.ConnectionString after opening. The owned-connection test incorrectly reused that sanitized property and failed authentication even though the singleton factory was now loaded correctly. Keep the original disposable CI input in the fixture and pass it directly to ProviderFactory.Create. Do not log it or change driver credential-retention settings. All seven other actual-engine scenarios passed in run 35769035270, including imperative/fluent/preview timestamp defaults, metadata, default removal, nullability changes, foreign keys, DML rollback and history restart. The complete matrix must pass on this commit before admitting HANA support. --- src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs b/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs index 00618c45..fe5aab94 100644 --- a/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs +++ b/src/Migrator.Tests/Providers/Hana/HanaProviderTests.cs @@ -16,13 +16,15 @@ namespace Migrator.Tests.Providers.Hana; public class HanaProviderTests { private HanaConnection connection; + private string connectionString; private ITransformationProvider provider; private string schema; [SetUp] public void SetUp() { - connection = new HanaConnection(Environment.GetEnvironmentVariable("MIGRATOR_HANA") - ?? "Server=localhost:39041;UserID=SYSTEM;Password=MgT9ci7Q4xZ2"); + connectionString = Environment.GetEnvironmentVariable("MIGRATOR_HANA") + ?? "Server=localhost:39041;UserID=SYSTEM;Password=MgT9ci7Q4xZ2"; + connection = new HanaConnection(connectionString); connection.Open(); schema = "MIGRATOR_" + Guid.NewGuid().ToString("N").ToUpperInvariant(); using var command = connection.CreateCommand(); @@ -43,7 +45,7 @@ public void TearDown() [Test] public void ConnectionStringFactoryOpensAndDisposesOwnedConnection() { - using var owned = ProviderFactory.Create(ProviderTypes.Hana, connection.ConnectionString, schema); + using var owned = ProviderFactory.Create(ProviderTypes.Hana, connectionString, schema); Assert.That(Convert.ToInt32(owned.ExecuteScalar("SELECT 1 FROM DUMMY")), Is.EqualTo(1)); } From e5f6b70e82ab02148d220ca4b3d95522956d44fe Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:18:47 +0200 Subject: [PATCH 33/34] Document v13 APIs and qualify source examples and package identity Update README and homepage quick starts to reference unreleased source explicitly and create primary keys as table constraint objects. Document RawSql defaults, semantic collation limits, effective scopes and the actual-engine admission gate for additional database providers. Refresh the framework comparison's source pin and SQLite constraint/index semantics; distinguish implemented v13 work from future design candidates in the migration guide. Set core, optional DI integration and CLI package metadata to 13.0.0-preview.1 and the core assembly/file versions to 13.0.0.0. Enable generated core version metadata while preserving manually declared title/description. No package is published. Validation: rebuilt solution and 87 unit tests; extracted matching README/homepage C# quick start compiled and migrated SQLite; fluent sample exercised preview, migration and automatic reversal. Inspected the homepage on desktop and a 390-pixel mobile viewport, confirmed no document overflow, and retained scrolling code blocks. Packed the v13 CLI locally. Live matrix results remain required on the PR head and linked source revision. --- README.md | 31 +++++++- docs/assets/site.css | 3 + docs/index.html | 52 +++++++++---- docs/migration-framework-comparison.md | 78 ++++++++++++------- docs/migration-guide-12.1-to-13.md | 6 +- ...ator.Extensions.DependencyInjection.csproj | 2 +- .../DotNetProjects.Migrator.Tool.csproj | 2 +- src/Migrator/DotNetProjects.Migrator.csproj | 10 ++- 8 files changed, 131 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 2470d152..1c4ce29a 100644 --- a/README.md +++ b/README.md @@ -58,12 +58,14 @@ Building the `.slnx` solution requires an SDK that understands that format, such ## Quick start +This example targets **unreleased v13 source**. Clone/check out the upgrade branch before running these commands from the repository root. For published 12.1, follow its version-specific API; see the [migration guide](docs/migration-guide-12.1-to-13.md). + ### 1. Create a migration host ```sh dotnet new console -n MigrationDemo -f net9.0 cd MigrationDemo -dotnet add package DotNetProjects.Migrator +dotnet add reference ../src/Migrator/DotNetProjects.Migrator.csproj dotnet add package Microsoft.Data.Sqlite --version 9.0.7 ``` @@ -82,8 +84,8 @@ public class CreateUsers : Migration { Database.AddTable("Users", new Column("Id", DbType.Int32) { IsNullable = false }, - new Column("Name", DbType.String, 255)); - Database.AddPrimaryKey("PK_Users", "Users", "Id"); + new Column("Name", DbType.String, 255), + new PrimaryKeyConstraint("PK_Users", "Id")); } public override void Down() @@ -319,3 +321,26 @@ The package declares **Mozilla Public License 1.1 (MPL-1.1)** in its [project me ### Version 13 source changes The unreleased v13 stack separates columns from named table constraints and removes the old column flags and duplicate fluent builder. See the [12.1-to-13 migration guide](docs/migration-guide-12.1-to-13.md) before recompiling migrations. These source features are not claims about the published 12.1 NuGet package. + + +### SQL expressions and collations in v13 + +```csharp +new Column("Id", DbType.String, 27) { DefaultValue = RawSql.Insert("ksuid_new()") }; +builder.Create.Table("Events").WithColumn("Id").AsString(27) + .WithDefaultValue(RawSql.Insert("ksuid_new()")); + +new Column("Name", DbType.String, 100) { Collation = Collation.CaseInsensitive }; +builder.Create.Table("Names").WithColumn("Name").AsString(100) + .WithCollation(Collation.CaseInsensitive); +``` + +SQL expressions are trusted migration code and must exist on the target database. +Ordinary string defaults remain quoted literals. Semantic collations have explicit +provider limits; SQLite's `AsciiIgnoreCase` never substitutes for Unicode folding. +Use `Collation.Named("provider_name")` for a specific language or installed collation. +See the [mapping and migration guide](docs/migration-guide-12.1-to-13.md#explicit-sql-defaults-and-semantic-collations). + +Additional engines are admitted only with passing real-database CI. +[SAP HANA qualification and deferred engine requirements](docs/additional-database-qualification.md) +cover the current FluentMigrator gaps without claiming untested support. diff --git a/docs/assets/site.css b/docs/assets/site.css index 3af8f11d..1c2286ab 100644 --- a/docs/assets/site.css +++ b/docs/assets/site.css @@ -797,3 +797,6 @@ footer .brand { scroll-behavior: auto; } } + +#version-13 .snippet { margin: 1rem 0; } +#version-13 p + p { margin-top: 1rem; } diff --git a/docs/index.html b/docs/index.html index 2a19fffe..a088649d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -65,10 +65,9 @@

Database changes.
Part of your code.

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"); + new Column("Id", DbType.Int32) { IsNullable = false }, + new Column("Name", DbType.String, 255), + new PrimaryKeyConstraint("PK_Users", "Id")); } public override void Down() @@ -130,6 +129,30 @@

Separate histories by scope

+
+
+
+

VERSION 13 · IN REVIEW

Explicit definitions, shared authoring.

+

Breaking source changes. These features are not yet a released NuGet version.

+
+

Define primary, unique, foreign-key and check constraints as table objects. + The imperative and fluent APIs share column definitions, trusted SQL defaults and typed collation requests.

+
new Column("Id", DbType.String, 27)
+    { DefaultValue = RawSql.Insert("ksuid_new()") };
+
+builder.Create.Table("Events")
+    .WithColumn("Id").AsString(27)
+    .WithDefaultValue(RawSql.Insert("ksuid_new()"))
+    .WithColumn("Name").AsString(100)
+    .WithCollation(Collation.CaseInsensitive);
+

The target database must supply the SQL function. Collation mappings have explicit provider limits: + SQLite ASCII folding does not satisfy a Unicode case-insensitive request. + Read the 12.1-to-13 migration guide ↗.

+

Additional databases require passing real-engine CI. + SAP HANA is being qualified ↗; + Redshift, Snowflake and Db2 for IBM i remain unsupported.

+
+
Separate histories by scope

From code to schema.

- A minimal SQLite example.
Use a .NET 9 console project for - the current source. + A minimal SQLite example for unreleased v13 source.
+ Use .NET 9 and a checkout of the upgrade branch.

1 -

Install the packages

+

Reference the v13 source

- Add Migrator and an ADO.NET driver. This example passes an open - connection directly to the provider. + Run these commands from your Migrator.NET checkout. This example references + the source project and passes an open SQLite connection to the provider.

View package versions on NuGet ↗Install the packages
dotnet new console -n MigrationDemo -f net9.0
 cd MigrationDemo
-dotnet add package DotNetProjects.Migrator
+dotnet add reference ../src/Migrator/DotNetProjects.Migrator.csproj
 dotnet add package Microsoft.Data.Sqlite --version 9.0.7
@@ -201,8 +224,8 @@

Describe the change

{ Database.AddTable("Users", new Column("Id", DbType.Int32) { IsNullable = false }, - new Column("Name", DbType.String, 255)); - Database.AddPrimaryKey("PK_Users", "Users", "Id"); + new Column("Name", DbType.String, 255), + new PrimaryKeyConstraint("PK_Users", "Id")); } public override void Down() @@ -260,9 +283,8 @@

Run pending migrations

diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index 817c70c9..c1740544 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 [`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. +Migrator findings are pinned to v13 upgrade-stack commit [`06b5f2a`][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) [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), [#180](https://github.com/dotnetprojects/Migrator.NET/pull/180) and [#181](https://github.com/dotnetprojects/Migrator.NET/pull/181), **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) @@ -151,6 +151,26 @@ Evidence: [Migrator runner][m-runner] and [execution][m-execution]; [FluentMigra 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. +## Version 13 authoring and database additions + +The v13 source uses `PrimaryKeyConstraint`, `UniqueConstraint`, `CheckConstraint` +and `ForeignKeyConstraint` objects instead of key/uniqueness flags on columns. +`IsNullable`, `IsIdentity` and `IsUnsigned` describe column attributes. The duplicate +legacy schema builder is removed; see the [migration guide](migration-guide-12.1-to-13.md). + +Both APIs accept `RawSql.Insert("ksuid_new()")` as a default expression; ordinary +strings remain values. Typed `Collation` presets resolve supported case/accent +comparison intent or fail explicitly; installed provider names remain available. +This does not make arbitrary SQL functions or linguistic ordering portable. +See [FluentMigrator's raw SQL helper](https://fluentmigrator.github.io/basics/raw-sql.html) +for the corresponding upstream default-expression feature. + +Current additional FluentMigrator engines are **SAP HANA, Redshift, Snowflake and +Db2 for IBM i**. HANA's actual-engine qualification is tracked in +[PR #182](https://github.com/dotnetprojects/Migrator.NET/pull/182); the other engines +require external test infrastructure. None is advertised as an implemented Migrator +provider. [Qualification requirements and primary sources](additional-database-qualification.md). + ## SQLite emulation comparison ### What emulation means @@ -204,14 +224,14 @@ Methods refer to the pinned [SQLite provider][m-sqlite]. Tests illustrate eviden | `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]. | +| `AddPrimaryKey` | Adds an explicit primary-key constraint and rebuilds without reordering physical columns. | Composite key order and actual constraint names are preserved; `PrimaryKeyExists` matches the name. [Tests][t-pk]. | +| `RemovePrimaryKey` | Removes the primary-key definition and identity attribute; 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]. | +| `RemoveAllIndexes` | Drops ordinary indexes; preserves declared unique constraints. | Unique indexes are indexes; constraint-backed indexes remain owned by their constraints. [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]. | @@ -316,7 +336,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 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. +The master baseline (`b7ae95c`) passed 139 SQLite tests with one skipped default-removal test. The earlier runner 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 earlier 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**. @@ -339,29 +359,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/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/ +[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Migrator.cs +[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/MigrationLoader.cs +[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/MigrationExecution.cs +[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Framework/Migration.cs +[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Framework/ITransformationProvider.cs +[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Providers/TransformationProvider.cs +[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/ProviderFactory.cs +[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/docs/live-database-tests.md +[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs +[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/06b5f2a8321d1ef94559ead9406c01c99adb658d/ [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 @@ -408,3 +428,5 @@ When updating: [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 + +V13 revision `06b5f2a` passes 87 unit and 195 SQLite tests locally; removed tests covered deleted APIs. Live results are recorded in [run 35766196473](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35766196473); check its conclusion before treating this revision as validated. diff --git a/docs/migration-guide-12.1-to-13.md b/docs/migration-guide-12.1-to-13.md index 0acc1b5b..c19567c5 100644 --- a/docs/migration-guide-12.1-to-13.md +++ b/docs/migration-guide-12.1-to-13.md @@ -142,7 +142,7 @@ 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. +Typed constraints with ordered metadata, the SQLite constraint tokenizer, explicit SQL defaults, semantic collations and removal of the duplicate authoring API are implemented in this source stack. Typed schema-qualified identifiers, deterministic naming conventions and a broader provider-capability model remain candidates; they are not implemented features. ## Explicit SQL defaults and semantic collations @@ -207,3 +207,7 @@ request is not translated to an Oracle index-organized table. Structured metadata preserves SQL Server nonclustered primary keys and Oracle ordered foreign-key pairs/delete actions. Foreign-key constructor arrays are copied, matching primary/unique definitions, so later caller-array edits cannot change the key. + +## Build and package identity + +Source builds now identify the core, optional DI package and CLI as 13.0.0-preview.1. The core assembly and file versions are 13.0.0.0; generated assembly metadata is enabled while preserving its existing title and description. Recompile consumers of the breaking API and update assembly/version binding assumptions. These metadata changes do not publish a package. diff --git a/src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj b/src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj index e8f1027b..91cb4ae0 100644 --- a/src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj +++ b/src/Migrator.Extensions.DependencyInjection/DotNetProjects.Migrator.Extensions.DependencyInjection.csproj @@ -1,5 +1,5 @@ - net9.09.0.0MPL-1.1Optional dependency injection, options and logging integration for Migrator.NET. + net9.013.0.0-preview.1MPL-1.1Optional dependency injection, options and logging integration for Migrator.NET. diff --git a/src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj b/src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj index 0a0f2501..048b0656 100644 --- a/src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj +++ b/src/Migrator.Tool/DotNetProjects.Migrator.Tool.csproj @@ -1,5 +1,5 @@ - Exenet9.0enabletruemigratorDotNetProjects.Migrator.Tool9.0.0MPL-1.1 + Exenet9.0enabletruemigratorDotNetProjects.Migrator.Tool13.0.0-preview.1MPL-1.1 diff --git a/src/Migrator/DotNetProjects.Migrator.csproj b/src/Migrator/DotNetProjects.Migrator.csproj index 83bd3d3c..f285e83b 100644 --- a/src/Migrator/DotNetProjects.Migrator.csproj +++ b/src/Migrator/DotNetProjects.Migrator.csproj @@ -2,7 +2,9 @@ net9.0 - false + true + false + false DotNetProjects.Migrator DotNetProjects.Migrator latest @@ -16,9 +18,9 @@ True https://github.com/dotnetprojects/Migrator.NET MPL-1.1 - 9.0.0.0 - 9.0.0.0 - 9.0.0.0 + 13.0.0.0 + 13.0.0.0 + 13.0.0-preview.1 From 253b032e7d5cdca66c7951c89178f0dd16abd0fb Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 22 Sep 2026 20:54:43 +0200 Subject: [PATCH 34/34] Document HANA qualification, refresh v13 evidence and remove obsolete test claim Update README, homepage and framework comparison to include the new HANA source provider, its mandatory SAP-engine CI job, tested scope and explicit limitations. Pin comparison source links to eabec55 and link the actual HANA matrix; retain Redshift, Snowflake and Db2 for IBM i as deferred until real CI endpoints exist. Distinguish unreleased source from NuGet and document the custom-host requirement for SAP's driver, HANA default-expression restrictions and DDL autocommit. Refresh the 81-issue audit with the v13 explicit-constraint resolution of #132, which remains open until merge. Remove the obsolete SQL Server test named as a uniqueness-removal regression that no longer created uniqueness; behavioral preservation/removal and duplicate-data tests already cover the replacement API. Correct the removed SchemaBuilder link, scope guidance, source-review links and local server test instructions, preserving semicolons when exporting credentials. Validation: solution build; 87 unit and 197 SQLite tests; README/homepage C# quick start and fluent preview/migration/reversal example; CLI package built locally. Homepage inspected on desktop and 390px mobile with no document overflow. Rebased PR #181 passed all twelve checks in run 35769034542. HANA and this final PR require successful checks at their respective heads; no merge, package publication or Pages deployment is performed here. --- README.md | 5 +- docs/README.md | 6 +- docs/additional-database-qualification.md | 9 ++- docs/index.html | 13 ++-- docs/issue-audit.md | 6 +- docs/live-database-tests.md | 14 +++- docs/migration-framework-comparison.md | 73 +++++++++++-------- ...ransformationProvider_ChangeColumnTests.cs | 19 +---- 8 files changed, 76 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 1c4ce29a..9e43a63b 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,7 @@ public override void Down() } ``` -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). +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 the [MigrationBuilder fluent API](src/Migrator/Framework/Fluent/MigrationBuilder.cs). ## Database providers @@ -242,6 +242,7 @@ The [provider factory](src/Migrator/ProviderFactory.cs) contains these database | IBM Informix | `IBM_Informix` | | Firebird | `Firebird` | | Ingres | `Ingres` | +| SAP HANA (v13 source) | `Hana` | | 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). @@ -342,5 +343,5 @@ Use `Collation.Named("provider_name")` for a specific language or installed coll See the [mapping and migration guide](docs/migration-guide-12.1-to-13.md#explicit-sql-defaults-and-semantic-collations). Additional engines are admitted only with passing real-database CI. -[SAP HANA qualification and deferred engine requirements](docs/additional-database-qualification.md) +[SAP HANA provider scope, CI evidence and deferred engine requirements](docs/additional-database-qualification.md) cover the current FluentMigrator gaps without claiming untested support. diff --git a/docs/README.md b/docs/README.md index 99d7775c..4c809518 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ Open http://localhost:8766. Content and navigation work without JavaScript. Copy ## Publish on GitHub Pages -1. In the repository's **Settings → Pages → Build and deployment**, set **Source** to **GitHub Actions**. +1. In the repository's **Settings → Pages → Build and deployment**, set **Source** to **GitHub Actions**. 2. Merge the site and `.github/workflows/pages.yml` into `master`. 3. The workflow publishes only `docs/`. After enabling Pages, you can also run **Deploy homepage to GitHub Pages** manually on `master`. @@ -24,6 +24,6 @@ All site assets use relative URLs, so the repository subpath works without a cus ## Keep the comparison accurate -The comparison distinguishes source capabilities from guarantees about released packages or database compatibility. Update the review date and source links together when reviewing it. Avoid equating transaction rollback with reversing completed migrations, treating a provider enum as a support guarantee, or treating scoped history as a migration discovery filter. +The comparison distinguishes source capabilities from guarantees about released packages or database compatibility. Update the review date and source links together when reviewing it. Avoid equating transaction rollback with reversing completed migrations, treating a provider enum as a support guarantee, or assuming scopes isolate physical tables. The v13 runner filters explicitly scoped migrations and lets unscoped migrations inherit its effective scope. -The quick start targets the current source's .NET 9 API. Check the selected NuGet release's target frameworks. The SQLite driver version matches the repository test dependency. Validate authoring and runner snippets together when changing them. +The quick start targets the unreleased v13 source's .NET 9 API and references the source project. Check the selected NuGet release's target frameworks. The SQLite driver version matches the repository test dependency. Validate authoring and runner snippets together when changing them. diff --git a/docs/additional-database-qualification.md b/docs/additional-database-qualification.md index 577e3664..403d02b1 100644 --- a/docs/additional-database-qualification.md +++ b/docs/additional-database-qualification.md @@ -7,7 +7,7 @@ This is an implementation gate, not a claim of released provider support. | Additional engine | Real-engine GitHub Actions route | Current disposition | | --- | --- | --- | -| SAP HANA | Official HANA Express Linux container and SAP's .NET driver, disposable schema | Infrastructure passed; provider and mandatory matrix tests added, pending their actual-engine results | +| SAP HANA | Official HANA Express Linux container and SAP's .NET driver, disposable schema | Implemented in the v13 source stack with a mandatory actual-engine matrix job; require green PR checks before merge | | Amazon Redshift | AWS test warehouse/serverless endpoint with CI credentials, network access and resource cleanup | No configured test infrastructure; defer provider | | Snowflake | Snowflake test account, warehouse, credentials and disposable database/schema | No configured test infrastructure; defer provider | | Db2 for IBM i | IBM i endpoint on Power infrastructure and compatible .NET/ODBC driver | No configured test infrastructure; defer provider | @@ -30,7 +30,8 @@ and catalog checks. Any startup or behavioral failure fails the job. Logs and runner resource evidence are retained; cleanup runs independently of test success. The prerequisite probe passed in [run 35766200488](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35766200488). The standalone workflow is replaced by the mandatory Hana job in the complete database matrix; the probe project remains reproducible evidence. -Provider admission now requires the new matrix coverage for +The provider matrix at source `eabec55` is recorded in [run 35770116342](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35770116342). +Provider admission requires this matrix to pass, covering imperative/fluent schema creation, constraint metadata, data, migration history, restart/rollback, preview parity and explicit unsupported operations. Do not mark a provider supported on the basis of SQL string tests or a skipped secret-gated job. @@ -61,7 +62,9 @@ retains HANA's default DDL autocommit behavior; data rollback does not prove DDL Use explicit SQL for tenant administration, computed columns, specialized indexes, collation configuration, and provider-specific data types without a mapped CLR type. The provider rejects unsupported included/filtered/clustered indexes and semantic -collation requests instead of ignoring them. +collation requests instead of ignoring them. The packaged CLI does not bundle the SAP driver; +use the library runner in a host that references the SAP package. Raw defaults retain HANA's +engine restrictions: CURRENT_TIMESTAMP is valid, whereas arbitrary LOWER(...) defaults are not. [HANA constraints](https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/209f7cf5751910149d9ce6b033d8ddce.html), [referential constraints](https://help.sap.com/docs/SAP_HANA_PLATFORM/4fe29514fd584807ac9f2a04f6754767/20ccc0a175191014901b88e6bc175c44.html), diff --git a/docs/index.html b/docs/index.html index a088649d..516efd0e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -149,7 +149,7 @@

Separate histories by scope

SQLite ASCII folding does not satisfy a Unicode case-insensitive request. Read the 12.1-to-13 migration guide ↗.

Additional databases require passing real-engine CI. - SAP HANA is being qualified ↗; + SAP HANA provider in v13 review ↗; Redshift, Snowflake and Db2 for IBM i remain unsupported.

@@ -325,6 +325,7 @@

Additional dialects in source

  • Firebird
  • Ingres
  • Sybase
  • +
  • SAP HANA (v13 source)
  • @@ -333,12 +334,12 @@

    Additional dialects in source

    server or driver version. Schema operations and transactional DDL vary by provider. Check the provider factory and provider tests for your database. @@ -364,7 +365,7 @@

    Choose by how you work.

    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. + Read the runner guide and limitations. Follow the PR stack.

    @@ -374,11 +375,11 @@

    Choose by how you work.

    Read the detailed feature comparison (Markdown) →
    Explore SQLite emulation, preservation limits and framework differences → diff --git a/docs/issue-audit.md b/docs/issue-audit.md index 477cfd4b..8ba45d86 100644 --- a/docs/issue-audit.md +++ b/docs/issue-audit.md @@ -1,10 +1,10 @@ # 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. +Reviewed issue set: 81 issues (23 open and 58 closed at the start). Baseline: master `b7ae95c`; upgrade work is in the stack #173, #174, #175, #177, #178, #180, #181, #182 and #183. 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 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). 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. +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). V13 source 746bd3c passed the complete existing-provider matrix and coverage gate in [run 35766920321](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35766920321), including 87 unit and 197 SQLite cases. HANA admission has separate actual-engine evidence. Later fixes require their own green checks. | Issue | Disposition | Reproduction / relevant evidence / remaining work | | --- | --- | --- | @@ -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; adoption and composite-rejection regressions passed live SQL Server in run 35741656276. | +| [#132](https://github.com/dotnetprojects/Migrator.NET/issues/132) SQL Server does not remove the unique index on ChangeColumn() with no ColumnProperty.Unique | Resolved by v13 explicit constraints in PR #181; await merge | ColumnProperty.Unique and implicit column-owned uniqueness are removed. ChangeColumn preserves explicit constraints and indexes; use RemoveConstraint or RemoveIndex after inspecting metadata. Live SQL Server tests ChangeColumnPreservesExplicitUniqueFromTableOrColumnCreation, ExplicitUniqueRemovalAllowsDuplicates and ChangeColumn_DoesNotRemoveUserOwnedUniqueOrMutateDefinition passed in run 35766920321. The migration guide documents this intentional breaking replacement. | | [#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. | diff --git a/docs/live-database-tests.md b/docs/live-database-tests.md index 8ae1d72c..a1e7f9db 100644 --- a/docs/live-database-tests.md +++ b/docs/live-database-tests.md @@ -15,9 +15,10 @@ The pull-request workflow runs independent jobs on GitHub-hosted Ubuntu 22.04 wi | Firebird | `firebirdsql/firebird:5.0.3` | FirebirdSql.Data.FirebirdClient 10.3.4 | | Db2 | `icr.io/db2_community/db2:11.5.9.0` | Net.IBM.Data.Db2-lnx 9.0.0.400 | | Informix | `icr.io/informix/informix-developer-database:15.0.1.0.3` | Informix.Net.Core-lnx 4.1501.2.2026 | +| Hana | `saplabs/hanaexpress:2.00.088.00.20251110.1` | Sap.Data.Hana.Net.v8.0 2.30.27 | | Sybase | `datagrip/sybase:16.0` (ASE developer image) | AdoNetCore.AseClient 0.19.2 | -The IBM Linux packages and ASE client are conditional test-project dependencies selected by `-p:LiveDatabase=Db2`, `Informix`, or `Sybase`. They do not become library dependencies. The library's provider identifiers and public API remain unchanged. Db2 and Informix containers need privileged mode. Image tags are fixed versions; container inspection artifacts record the actual downloaded image IDs. +The IBM Linux packages and ASE client are conditional test-project dependencies selected by `-p:LiveDatabase=Db2`, `Informix`, or `Sybase`. They do not become library dependencies. The SAP client is also a test-only dependency; the core loads its factory dynamically. Db2 and Informix containers need privileged mode. Image tags are fixed versions; container inspection artifacts record the actual downloaded image IDs. ## Coverage and isolation @@ -25,7 +26,9 @@ The IBM Linux packages and ASE client are conditional test-project dependencies Every test creates a uniquely named database (a schema for Db2, an independent server-side file for Firebird). Connections disable pooling. Teardown disposes the provider and drops that database/schema; Db2 removes tables in dependency order first. Migration cycles reuse the same isolated store to exercise repeatability even on engines whose DDL commits automatically. ASE test databases enable full logging for ALTER TABLE and allow DDL in transactions and allocate 32 MB of data plus a separate 16 MB log allocation to accommodate the image's model database. -Existing SQL Server, PostgreSQL, Oracle and SQLite suites continue to run in full. The Unit job uses the complement of all database categories. An audit compares NUnit's discovery count against the union of all job results and rejects missing or duplicate test assignments. Each job rejects zero executed tests; new suites also reject skips. Existing ignored tests retain their documented reasons: generic default removal (issue #139) and a SQL Server column-change regression (issue #132). TRX and NUnit XML expose each reason for review. +The Hana suite creates a disposable schema per test and covers imperative/fluent/generated schema creation, timestamp expression defaults, constraint/index metadata, data and nullability/default changes, caller-owned connections, DML rollback, and migration history restart/downgrade. Its unsupported-operation tests reject unavailable capabilities. + +Existing SQL Server, PostgreSQL, Oracle and SQLite suites continue to run in full. The Unit job uses the complement of all database categories. An audit compares NUnit's discovery count against the union of all job results and rejects missing or duplicate test assignments. Each job rejects zero executed tests; new suites also reject skips. Previously ignored default-removal and SQL Server uniqueness cases have behavioral replacements. TRX and NUnit XML expose any remaining ignored case and its reason; a skipped case is never evidence of support. Readiness and startup are bounded; database jobs time out after 35 minutes. Startup logs, container logs/inspection, TRX and NUnit XML are uploaded on success or failure. Registry downloads may retry; test failures never do. A new commit cancels an obsolete run. @@ -43,7 +46,11 @@ For a server-backed suite, use Linux with Docker, .NET 9 and PowerShell (`pwsh`) ```bash database=MySQL # or a server job name from the table +export RUNNER_TEMP="$(mktemp -d)" +export GITHUB_ENV="$RUNNER_TEMP/database.env" +touch "$GITHUB_ENV" bash .github/scripts/start-database.sh "$database" +while IFS= read -r setting; do export "$setting"; done < "$GITHUB_ENV" dotnet build Migrator.slnx -p:LiveDatabase="$database" pwsh -File .github/scripts/test.ps1 -Database "$database" docker logs migrator-db @@ -65,7 +72,7 @@ export INFORMIXDIR="$output/native" export LD_LIBRARY_PATH="$output/native/lib:$output/native/lib/cli:$output/native/lib/esql" ``` -New suites accept `MIGRATOR_MYSQL`, `MIGRATOR_MARIADB`, `MIGRATOR_FIREBIRD`, `MIGRATOR_DB2`, `MIGRATOR_INFORMIX`, or `MIGRATOR_SYBASE` connection-string overrides. Use disposable servers with administrative database/schema creation permissions. Defaults match the startup script. ASE additionally expects the disposable `migrator_data` and `migrator_log` devices initialized by that script. Existing suites read `appsettings.json` through ConfigurationReader, with a `MIGRATOR_` plus uppercased configuration-key override. +New suites accept `MIGRATOR_MYSQL`, `MIGRATOR_MARIADB`, `MIGRATOR_FIREBIRD`, `MIGRATOR_DB2`, `MIGRATOR_INFORMIX`, `MIGRATOR_SYBASE`, or `MIGRATOR_HANA` connection-string overrides. Use disposable servers with administrative database/schema creation permissions. Defaults match the startup script. ASE additionally expects the disposable `migrator_data` and `migrator_log` devices initialized by that script. Existing suites read `appsettings.json` through ConfigurationReader, with a `MIGRATOR_` plus uppercased configuration-key override. To reproduce the assignment audit, download all `test-results-*` artifacts from a single completed workflow into `TestResults`, preserving their per-database directories, then run: @@ -75,6 +82,7 @@ python3 .github/scripts/verify-test-coverage.py TestResults ## Engine and provider limits +- HANA Express startup needs Docker and the kernel settings in `start-hana.sh`; it takes several minutes. The test script creates and removes schemas, so its account needs those permissions. HANA defaults retain engine restrictions: `CURRENT_TIMESTAMP` is supported, arbitrary function calls such as `LOWER(...)` are not valid default clauses. DDL may autocommit; the provider rejects whole-session transactional DDL, native locking and tenant administration. See [additional database qualification](additional-database-qualification.md) for tested scope and deferred engines. - MySQL/MariaDB DDL may commit automatically; the suite uses independent databases instead of relying on rollback. Modern pinned versions enforce CHECK constraints. - Firebird identity columns require Firebird 3 or newer; this suite tests version 5. SQL cannot enumerate all server database files, so `GetDatabases` returns the attached database. Firebird has no general table rename operation. - Db2 primary-key and unique-constraint columns must be NOT NULL. Column changes can require REORG, which the provider performs. Foreign-key updates are restrictive; supported delete actions are translated separately. `GetDatabases` returns the current server database, not a client catalog. diff --git a/docs/migration-framework-comparison.md b/docs/migration-framework-comparison.md index c1740544..8aead7e4 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 v13 upgrade-stack commit [`06b5f2a`][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) [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), [#180](https://github.com/dotnetprojects/Migrator.NET/pull/180) and [#181](https://github.com/dotnetprojects/Migrator.NET/pull/181), **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 v13 upgrade-stack commit [`eabec55`][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) [#177](https://github.com/dotnetprojects/Migrator.NET/pull/177), [#180](https://github.com/dotnetprojects/Migrator.NET/pull/180) [#181](https://github.com/dotnetprojects/Migrator.NET/pull/181) and [#182](https://github.com/dotnetprojects/Migrator.NET/pull/182), **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) @@ -143,7 +143,7 @@ Evidence: [Migrator runner][m-runner] and [execution][m-execution]; [FluentMigra | 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. | +| Migrator | Source dialects + separate ADO.NET drivers. Live CI covers SQLite, SQL Server, PostgreSQL, Oracle, MySQL, MariaDB, Firebird, Db2, Informix, Sybase and the new HANA job; 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. | @@ -165,11 +165,17 @@ This does not make arbitrary SQL functions or linguistic ordering portable. See [FluentMigrator's raw SQL helper](https://fluentmigrator.github.io/basics/raw-sql.html) for the corresponding upstream default-expression feature. -Current additional FluentMigrator engines are **SAP HANA, Redshift, Snowflake and -Db2 for IBM i**. HANA's actual-engine qualification is tracked in -[PR #182](https://github.com/dotnetprojects/Migrator.NET/pull/182); the other engines -require external test infrastructure. None is advertised as an implemented Migrator -provider. [Qualification requirements and primary sources](additional-database-qualification.md). +The new **SAP HANA** source provider in [PR #182](https://github.com/dotnetprojects/Migrator.NET/pull/182) +has a mandatory GitHub Actions job using SAP's actual HANA Express engine and native +.NET driver. It covers schema/data operations, metadata, constraints, migration +history, restart, DML rollback and imperative/fluent/preview parity. DDL may +autocommit; native migration locking, whole-session transactional DDL and the +packaged CLI's online HANA host are not provided. + +The remaining additional FluentMigrator engines are **Redshift, Snowflake and +Db2 for IBM i**. They require external test infrastructure and remain deferred. +PostgreSQL tests do not qualify Redshift, and Db2 LUW tests do not qualify IBM i. +[Qualification requirements, CI evidence and primary sources](additional-database-qualification.md). ## SQLite emulation comparison @@ -359,29 +365,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/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Migrator.cs -[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/MigrationLoader.cs -[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/MigrationExecution.cs -[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Framework/Migration.cs -[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Framework/ITransformationProvider.cs -[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Providers/TransformationProvider.cs -[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/ProviderFactory.cs -[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/docs/live-database-tests.md -[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs -[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs -[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs -[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs -[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs -[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs -[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs -[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs -[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs -[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs -[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs -[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs -[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs -[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/06b5f2a8321d1ef94559ead9406c01c99adb658d/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs -[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/06b5f2a8321d1ef94559ead9406c01c99adb658d/ +[m-runner]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/Migrator.cs +[m-loader]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/MigrationLoader.cs +[m-execution]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/MigrationExecution.cs +[m-migration]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/Framework/Migration.cs +[m-api]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/Framework/ITransformationProvider.cs +[m-provider]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/Providers/TransformationProvider.cs +[m-factory]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/ProviderFactory.cs +[m-live]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/docs/live-database-tests.md +[m-sqlite]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +[m-sqlite-model]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +[t-add-column]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddColumnTests.cs +[t-change-column]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_ChangeColumnTests.cs +[t-remove-column]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +[t-rename-column]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +[t-pk]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddPrimaryKeyTests.cs +[t-fk]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddForeignKeyTests.cs +[t-integrity]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_CheckForeignKeyIntegrityTests.cs +[t-uniques]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +[t-check]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs +[t-remove-constraints]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveAllConstraintsTests.cs +[t-recreate]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RecreateTable.cs +[t-sqlite-general]: https://github.com/dotnetprojects/Migrator.NET/blob/eabec5561c2c9c77847b9adbb7b67474e3c70a50/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderTests.cs +[m-revision]: https://github.com/dotnetprojects/Migrator.NET/tree/eabec5561c2c9c77847b9adbb7b67474e3c70a50/ [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 @@ -429,4 +435,9 @@ When updating: [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 -V13 revision `06b5f2a` passes 87 unit and 195 SQLite tests locally; removed tests covered deleted APIs. Live results are recorded in [run 35766196473](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35766196473); check its conclusion before treating this revision as validated. +The v13 column-model revision `11d6083` passed the complete existing-provider +matrix in [run 35769034542](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35769034542), +including 87 unit and 197 SQLite tests. The additional HANA source is pinned at +`eabec55` and its complete matrix is [run 35770116342](https://github.com/dotnetprojects/Migrator.NET/actions/runs/35770116342). +Require successful actual-engine checks on the final PR head before merging; +SQL-string assertions and skipped jobs do not qualify a new provider. diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs index fb511ef4..9162fc95 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_ChangeColumnTests.cs @@ -80,21 +80,4 @@ public void ChangeColumn_DoesNotRemoveUserOwnedUniqueOrMutateDefinition() Assert.That(definition.IsNullable, Is.False); } - [Test] - public void ChangeColumn_WithUniqueThenReChangeToNonUnique_UniqueConstraintShouldBeRemoved() - { - // Arrange - const string tableName = "TestTable"; - const string columnName = "TestColumn"; - - Provider.AddTable(tableName, new Column(columnName,DbType.Int32){IsNullable = false}); - - // Act - Provider.ChangeColumn(tableName, new Column(columnName,DbType.Int32){IsNullable = false}); - Provider.ChangeColumn(tableName, new Column(columnName,DbType.Int32){IsNullable = false}); - - // Assert - var indexes = Provider.GetIndexes(tableName); - Assert.That(indexes, Is.Empty); - } -} \ No newline at end of file +}