using System.Reflection; using System.Text; using Npgsql; namespace TaxBaik.Infrastructure.Data; public class MigrationRunner { private readonly string _connectionString; private readonly IDbConnectionFactory _connectionFactory; public MigrationRunner(string connectionString, IDbConnectionFactory connectionFactory) { _connectionString = connectionString; _connectionFactory = connectionFactory; } public async Task RunAsync() { await EnsureMigrationTableAsync(); await ExecutePendingMigrationsAsync(); } private async Task EnsureMigrationTableAsync() { using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = @" CREATE TABLE IF NOT EXISTS schema_migrations ( version VARCHAR(50) PRIMARY KEY, description VARCHAR(500), installed_on TIMESTAMPTZ DEFAULT NOW() );"; await cmd.ExecuteNonQueryAsync(); } private async Task ExecutePendingMigrationsAsync() { var executedMigrations = await GetExecutedMigrationsAsync(); var migrations = GetAvailableMigrations(); foreach (var migration in migrations.OrderBy(x => x.Version)) { if (!executedMigrations.Contains(migration.Version)) { await ExecuteMigrationAsync(migration); } } } private async Task> GetExecutedMigrationsAsync() { var executed = new HashSet(); using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT version FROM schema_migrations ORDER BY version;"; using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) { executed.Add(reader.GetString(0)); } return executed; } private List GetAvailableMigrations() { var migrations = new List(); var assembly = Assembly.GetExecutingAssembly(); var resourceNames = assembly.GetManifestResourceNames() .Where(x => x.Contains("Migrations") && x.EndsWith(".sql")) .ToList(); foreach (var resourceName in resourceNames.OrderBy(x => x)) { var parts = resourceName.Split('.'); var fileName = parts[parts.Length - 2]; if (fileName.StartsWith("V")) { var version = fileName.Substring(1, fileName.IndexOf('_') - 1); var description = fileName.Substring(fileName.IndexOf('_') + 2); using var stream = assembly.GetManifestResourceStream(resourceName); using var reader = new StreamReader(stream); var sql = reader.ReadToEnd(); migrations.Add(new Migration { Version = version, Description = description, Sql = sql }); } } return migrations; } private async Task ExecuteMigrationAsync(Migration migration) { using var conn = new NpgsqlConnection(_connectionString); await conn.OpenAsync(); try { using var cmd = conn.CreateCommand(); cmd.CommandText = migration.Sql; await cmd.ExecuteNonQueryAsync(); using var insertCmd = conn.CreateCommand(); insertCmd.CommandText = "INSERT INTO schema_migrations (version, description) VALUES (@version, @description);"; insertCmd.Parameters.AddWithValue("@version", migration.Version); insertCmd.Parameters.AddWithValue("@description", migration.Description); await insertCmd.ExecuteNonQueryAsync(); Console.WriteLine($"✓ Migration {migration.Version} executed"); } catch (Exception ex) { Console.WriteLine($"✗ Migration {migration.Version} failed: {ex.Message}"); throw; } } private class Migration { public string Version { get; set; } public string Description { get; set; } public string Sql { get; set; } } }