From 547c26af3264beb34e24f4cc22f4bc6dd5a84e69 Mon Sep 17 00:00:00 2001 From: Russ Kollmansberger Date: Sun, 31 Aug 2025 12:03:10 -0500 Subject: [PATCH] Added migrations --- DbTools.csproj | 4 + EventArguments/MigrationCompletedEventArgs.cs | 20 ++++ EventArguments/MigrationProgressEventArgs.cs | 23 ++++ EventArguments/MigrationStartedEventArgs.cs | 16 +++ Migrations.cs | 107 ++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 EventArguments/MigrationCompletedEventArgs.cs create mode 100644 EventArguments/MigrationProgressEventArgs.cs create mode 100644 EventArguments/MigrationStartedEventArgs.cs create mode 100644 Migrations.cs diff --git a/DbTools.csproj b/DbTools.csproj index b0e4f0c..6b0c0c6 100644 --- a/DbTools.csproj +++ b/DbTools.csproj @@ -42,8 +42,12 @@ + + + + diff --git a/EventArguments/MigrationCompletedEventArgs.cs b/EventArguments/MigrationCompletedEventArgs.cs new file mode 100644 index 0000000..17092e4 --- /dev/null +++ b/EventArguments/MigrationCompletedEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace DbTools.EventArguments { + public delegate void MigrationCompletedEventHandler(object sender, MigrationCompletedEventArgs e); + + public class MigrationCompletedEventArgs : EventArgs { + public string MigrationName { get; set; } + public bool WasSuccessful { get; set; } + public Exception Error { get; set; } + public int Remaining { get; set; } + + public MigrationCompletedEventArgs() { } + public MigrationCompletedEventArgs(string migrationName, int remaining, Exception error = null, bool wasSuccessful = true) { + MigrationName = migrationName; + WasSuccessful = error != null ? false : wasSuccessful; + Error = error; + Remaining = remaining; + } + } +} diff --git a/EventArguments/MigrationProgressEventArgs.cs b/EventArguments/MigrationProgressEventArgs.cs new file mode 100644 index 0000000..1575b97 --- /dev/null +++ b/EventArguments/MigrationProgressEventArgs.cs @@ -0,0 +1,23 @@ +using System; + +namespace DbTools.EventArguments { + public delegate void MigrationProgressEventHandler(object sender, MigrationProgressEventArgs e); + + public class MigrationProgressEventArgs : EventArgs { + public string StatusText { get; set; } + public int Total { get; set; } + public int Pending { get; set; } + public int Successful { get; set; } + public int Failed { get; set; } + + public MigrationProgressEventArgs() { } + + public MigrationProgressEventArgs(int total, int pending, int successful, int failed, string statusText = "") { + Total = total; + Pending = pending; + Successful = successful; + Failed = failed; + StatusText = statusText; + } + } +} diff --git a/EventArguments/MigrationStartedEventArgs.cs b/EventArguments/MigrationStartedEventArgs.cs new file mode 100644 index 0000000..362a4b8 --- /dev/null +++ b/EventArguments/MigrationStartedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace DbTools.EventArguments { + public delegate void MigrationStartedEventHandler(object sender, MigrationStartedEventArgs e); + + public class MigrationStartedEventArgs : EventArgs { + public string MigrationName { get; set; } + public int Remaining { get; set; } + + public MigrationStartedEventArgs() { } + public MigrationStartedEventArgs(string migrationName, int remaining) { + MigrationName = migrationName; + Remaining = remaining; + } + } +} diff --git a/Migrations.cs b/Migrations.cs new file mode 100644 index 0000000..c63f5f2 --- /dev/null +++ b/Migrations.cs @@ -0,0 +1,107 @@ +using DbTools.EventArguments; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace DbTools { + public class Migrations { + public string MigrationPath { get; set; } + public string CompletedMigrationFile { get; set; } = "migrations.txt"; + + public int Pending { get; private set; } + public int Successful { get; private set; } + public int Failed { get; private set; } + public int Total { get; private set; } + + public event MigrationProgressEventHandler MigrationProgressReported; + public event MigrationCompletedEventHandler MigrationCompleted; + public event MigrationStartedEventHandler MigrationStarted; + + public Migrations(string migrationPath) { + MigrationPath = migrationPath; + if (!Directory.Exists(MigrationPath)) { + Directory.CreateDirectory(MigrationPath); + } + } + + public string[] GetAvailableMigrations(string databaseName, bool includeFailed = false) { + string completedFile = Path.Combine(MigrationPath, CompletedMigrationFile); + List completedMigrations = new List(); + if (!File.Exists(completedFile)) { + File.WriteAllText(completedFile, ""); + } + + // migrations.txt format: filename.sql;date_time_performed;success=1-or-fail=0 + // data_6.0.12.123_01.sql;25252511;1 + string[] migrations = File.ReadAllLines(completedFile); + foreach (string migration in migrations) { + string[] parts = migration.Split(';'); + if (int.Parse(parts[2] ?? "0") != 0) { + completedMigrations.Add(migration); + } + } + + var migrationFiles = Directory.GetFiles(MigrationPath, databaseName + "_*.sql") + .Where(f => !completedMigrations.Contains(Path.GetFileName(f))) + .OrderBy(f => f); + + return migrationFiles.ToArray(); + } + + public void MarkMigrationComplete(string migrationName, bool wasSuccessful = true) { + File.AppendAllText(Path.Combine(MigrationPath, CompletedMigrationFile), + migrationName + ";" + DateTime.Now.ToString("yyyyMMddHHmmss") + ";" + Convert.ToInt16(wasSuccessful).ToString() + Environment.NewLine); + } + + public async Task ApplyMigrationsAsync(IDbConnection dbConnection, string databaseName, bool attemptFailed = false) { + await Task.Run(() => { + ApplyMigrations(dbConnection, databaseName, attemptFailed); + }); + } + + public void ApplyMigrations(IDbConnection dbConnection, string databaseName, bool attemptFailed = false) { + string[] migrationFiles = GetAvailableMigrations(databaseName, attemptFailed); + Total = migrationFiles.Count(); + Pending = Total; + Failed = 0; + Successful = 0; + + // Iterate through the migrations reporting progress along the way + foreach (var migration in migrationFiles) { + string migrationName = Path.GetFileNameWithoutExtension(migration); + MigrationStarted?.Invoke(this, new MigrationStartedEventArgs(migrationName, --Pending)); + MigrationProgressReported?.Invoke(this, new MigrationProgressEventArgs(Total, Pending, Successful, Failed, $"Started Migration '{migrationName}'...")); + + // TODO : Do the work + Exception error = null; + bool wasSuccessful = false; + try { + using (var cmd = dbConnection.CreateCommand()) { + cmd.CommandText = File.ReadAllText(migration); + if (cmd.ExecuteNonQuery() > 0) { + wasSuccessful = true; + } + wasSuccessful = false; + } + } catch (Exception ex) { + error = ex; + wasSuccessful = false; + } + + // Report progress + if (wasSuccessful) { + Successful++; + } else { + Failed++; + } + MigrationCompleted?.Invoke(this, new MigrationCompletedEventArgs(migrationName, Pending, error, wasSuccessful)); + MigrationProgressReported?.Invoke(this, new MigrationProgressEventArgs(Total, Pending, Successful, Failed, + (wasSuccessful ? "Successfully" : "Unsuccessfully") + $" Completed Migration '{migrationName}'.")); + MarkMigrationComplete(migrationName + ".sql", wasSuccessful); + } + } + } +}