using System.Reflection; using System.Text; using Npgsql; using TaxBaik.Domain.Interfaces; 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(); // Try file system first (for deployment), then embedded resources var migrationDirs = new[] { "./migrations", // relative "/home/kjh2064/taxbaik_active/migrations" // deployment }; var migrationPath = migrationDirs.FirstOrDefault(Directory.Exists); if (migrationPath != null && Directory.Exists(migrationPath)) { var files = Directory.GetFiles(migrationPath, "V*.sql").OrderBy(x => x); foreach (var file in files) { var fileName = Path.GetFileNameWithoutExtension(file); if (fileName.StartsWith("V")) { var version = fileName.Substring(1, fileName.IndexOf('_') - 1); var description = fileName.Substring(fileName.IndexOf('_') + 2); var sql = File.ReadAllText(file); 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 (Npgsql.PostgresException pgEx) when (pgEx.SqlState == "42P07") // relation already exists { // Already executed previously; mark as done Console.WriteLine($"ℹ Migration {migration.Version} already applied"); using var insertCmd = conn.CreateCommand(); insertCmd.CommandText = "INSERT INTO schema_migrations (version, description) VALUES (@version, @description) ON CONFLICT (version) DO NOTHING;"; insertCmd.Parameters.AddWithValue("@version", migration.Version); insertCmd.Parameters.AddWithValue("@description", migration.Description); await insertCmd.ExecuteNonQueryAsync(); } 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; } } }