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);
}