Files
taxbaik/TaxBaik.Infrastructure/Data/MigrationRunner.cs
T
kjh2064 1d7dd71011
TaxBaik CI/CD / build-and-deploy (push) Successful in 41s
fix: unify TaxBaik deployment around CI
2026-06-27 01:34:17 +09:00

177 lines
6.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<HashSet<string>> GetExecutedMigrationsAsync()
{
var executed = new HashSet<string>();
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<Migration> GetAvailableMigrations()
{
var migrations = new List<Migration>();
// 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 });
}
}
}
else
{
var assembly = Assembly.GetExecutingAssembly();
var resourceNames = assembly.GetManifestResourceNames()
.Where(x => x.Contains(".Migrations.V") && x.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x);
foreach (var resourceName in resourceNames)
{
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
continue;
using var reader = new StreamReader(stream);
var sql = reader.ReadToEnd();
var fileName = Path.GetFileNameWithoutExtension(resourceName);
var versionStart = fileName.IndexOf('V');
var versionEnd = fileName.IndexOf('_', versionStart + 1);
if (versionStart < 0 || versionEnd < 0)
continue;
var version = fileName.Substring(versionStart + 1, versionEnd - versionStart - 1);
var description = fileName.Substring(versionEnd + 1);
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; }
}
}