using Npgsql; using Server.WatchBody.Core.Dal.Src.Repository; using Server.WatchBody.Web.Db; namespace Server.WatchBody.Web.Services; /// /// Режим --mode migrate (З-36, requirements 2.3): применение файлов /// 2-db\migrations\ по возрастанию имён; файл не в журнале src.migration /// — выполняется целиком в транзакции (скрипт + запись журнала), сбой — /// откат и код возврата 1. Миграции идемпотентны (IF NOT EXISTS / /// OR REPLACE) — повторный запуск безопасен. /// public class MigrationRunner(PostgresConnectionString connectionString) { public int Apply(string migrationsDir) { if (!Directory.Exists(migrationsDir)) throw new DirectoryNotFoundException($"каталог миграций не найден: {migrationsDir}"); var repository = new MigrationRepository(connectionString); var applied = 0; foreach (var file in Directory.GetFiles(migrationsDir, "*.sql") .OrderBy(f => Path.GetFileName(f), StringComparer.Ordinal)) { var name = Path.GetFileName(file); if (repository.Select(name) != null) continue; var sql = File.ReadAllText(file); using (var scope = new System.Transactions.TransactionScope( System.Transactions.TransactionScopeAsyncFlowOption.Enabled)) { // скрипт и журнал — одна ambient-транзакция (оба соединения // Npgsql, Enlist по умолчанию); сбой → откат всего файла using (var connection = new NpgsqlConnection(connectionString.Value)) { connection.Open(); using var command = new NpgsqlCommand(sql, connection); command.ExecuteNonQuery(); } repository.Insert(name); scope.Complete(); } Console.WriteLine($"миграция применена: {name}"); applied++; } return applied; } }