using Npgsql; using StellaOps.Doctor.Models; using StellaOps.Doctor.Plugins; using StellaOps.Doctor.Plugins.Builders; namespace StellaOps.Doctor.Plugins.Database.Checks; /// /// Checks for failed or incomplete database migrations. /// public sealed class FailedMigrationsCheck : DatabaseCheckBase { /// public override string CheckId => "check.db.migrations.failed"; /// public override string Name => "Failed Migrations"; /// public override string Description => "Checks for failed or incomplete database migrations"; /// public override IReadOnlyList Tags => ["database", "migrations", "schema"]; /// protected override async Task ExecuteCheckAsync( DoctorPluginContext context, string connectionString, CheckResultBuilder result, CancellationToken ct) { await using var connection = await CreateConnectionAsync(connectionString, ct); // Check for Stella Ops migration tracking table var hasTrackingTable = await CheckTableExistsAsync(connection, "stella_migration_history", ct); if (!hasTrackingTable) { return result .Info("No migration tracking table found - using EF Core migrations only") .WithEvidence("Migration status", e => e .Add("TrackingTableExists", "false")) .Build(); } // Check for failed migrations in tracking table await using var cmd = new NpgsqlCommand(@" SELECT migration_id, status, error_message, applied_at FROM stella_migration_history WHERE status = 'failed' OR status = 'incomplete' ORDER BY applied_at DESC LIMIT 5", connection); var failedMigrations = new List<(string Id, string Status, string? Error)>(); await using var reader = await cmd.ExecuteReaderAsync(ct); while (await reader.ReadAsync(ct)) { failedMigrations.Add(( reader.GetString(0), reader.GetString(1), reader.IsDBNull(2) ? null : reader.GetString(2) )); } if (failedMigrations.Count > 0) { return result .Fail($"{failedMigrations.Count} failed/incomplete migration(s) found") .WithEvidence("Failed migrations", e => { e.Add("FailedCount", failedMigrations.Count.ToString()); for (int i = 0; i < failedMigrations.Count; i++) { var m = failedMigrations[i]; e.Add($"Migration_{i + 1}", $"{m.Id} ({m.Status})"); if (m.Error != null) { e.Add($"Error_{i + 1}", m.Error); } } }) .WithCauses( "Migration script has errors", "Database permission issues", "Concurrent migration attempts") .WithRemediation(r => r .AddManualStep(1, "Review migration logs", "Check application logs for migration error details") .AddManualStep(2, "Fix migration issues", "Resolve the underlying issue and retry migration") .AddShellStep(3, "Retry migrations", "dotnet ef database update")) .WithVerification("stella doctor --check check.db.migrations.failed") .Build(); } return result .Pass("No failed migrations found") .WithEvidence("Migration status", e => e .Add("FailedMigrations", "0") .Add("TrackingTableExists", "true")) .Build(); } private static async Task CheckTableExistsAsync(NpgsqlConnection connection, string tableName, CancellationToken ct) { await using var cmd = new NpgsqlCommand( $"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = '{tableName}')", connection); return Convert.ToBoolean(await cmd.ExecuteScalarAsync(ct)); } }