using System; using System.Collections.Generic; using System.Data; using System.Linq; using Microsoft.Extensions.Logging; namespace Migrato.Core; /// /// Orchestrates applying and reverting migrations against a database, via an /// public partial class MigrationRunner(IMigrationProvider provider, ILogger logger) { /// /// Reports the applied/pending status of each of /// public IReadOnlyList GetStatus(IDbConnection connection, IReadOnlyList migrations) { provider.EnsureVersionTable(connection); var applied = provider.GetAppliedMigrations(connection).ToDictionary(a => a.Version); return migrations .OrderBy(m => m.Version) .Select(m => applied.TryGetValue(m.Version, out var appliedMigration) ? new MigrationStatusEntry { Version = m.Version, Name = m.Name, IsApplied = true, AppliedAt = appliedMigration.AppliedAt } : new MigrationStatusEntry { Version = m.Version, Name = m.Name, IsApplied = false }) .ToList(); } /// /// Applies every migration in that has not already been applied, in version order. /// Stops and rethrows on the first failure; migrations applied before the failure remain applied. /// /// The migrations that were applied by this call public IReadOnlyList MigrateUp(IDbConnection connection, IReadOnlyList migrations) { LogCheckingVersionTableOnConnection(connection.ConnectionString); provider.EnsureVersionTable(connection); var appliedVersions = provider.GetAppliedMigrations(connection).Select(a => a.Version).ToHashSet(); var applied = new List(); foreach (var migration in migrations.OrderBy(m => m.Version)) { if (appliedVersions.Contains(migration.Version)) { continue; } LogRunningUpMigration($"{migration.Version}_{migration.Name}"); RunInTransaction(connection, migration.UpSql, transaction => provider.RecordMigration(connection, transaction, migration)); applied.Add(migration); LogFinishedUpMigration($"{migration.Version}_{migration.Name}"); } return applied; } /// /// Reverts the highest-versioned applied migration in /// /// The migration that was reverted, or null if none of were applied public Migration? MigrateDown(IDbConnection connection, IReadOnlyList migrations) { LogCheckingVersionTableOnConnection(connection.ConnectionString); provider.EnsureVersionTable(connection); var appliedVersions = provider.GetAppliedMigrations(connection).Select(a => a.Version).ToHashSet(); var target = migrations .Where(m => appliedVersions.Contains(m.Version)) .OrderByDescending(m => m.Version) .FirstOrDefault(); if (target is null) { return null; } LogRunningDownMigrationMigration($"{target.Version}_{target.Name}"); RunInTransaction(connection, target.DownSql, transaction => provider.RemoveMigration(connection, transaction, target.Version)); LogFinishedDownMigrationMigration($"{target.Version}_{target.Name}"); return target; } private static void RunInTransaction(IDbConnection connection, string sql, Action recordChange) { using var transaction = connection.BeginTransaction(); try { using (var command = connection.CreateCommand()) { command.Transaction = transaction; command.CommandText = sql; command.ExecuteNonQuery(); } recordChange(transaction); transaction.Commit(); } catch { transaction.Rollback(); throw; } } [LoggerMessage(LogLevel.Information, "Running up migration {migration}")] partial void LogRunningUpMigration(string migration); [LoggerMessage(LogLevel.Debug, "Checking version table on {connection}")] partial void LogCheckingVersionTableOnConnection(string connection); [LoggerMessage(LogLevel.Information, "Finished up migration {migration}")] partial void LogFinishedUpMigration(string migration); [LoggerMessage(LogLevel.Information, "running down migration {migration}")] partial void LogRunningDownMigrationMigration(string migration); [LoggerMessage(LogLevel.Information, "Finished down migration {migration}")] partial void LogFinishedDownMigrationMigration(string migration); }