Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Database/Checks/FailedMigrationsCheck.cs

112 lines
4.2 KiB
C#

using Npgsql;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Builders;
namespace StellaOps.Doctor.Plugins.Database.Checks;
/// <summary>
/// Checks for failed or incomplete database migrations.
/// </summary>
public sealed class FailedMigrationsCheck : DatabaseCheckBase
{
/// <inheritdoc />
public override string CheckId => "check.db.migrations.failed";
/// <inheritdoc />
public override string Name => "Failed Migrations";
/// <inheritdoc />
public override string Description => "Checks for failed or incomplete database migrations";
/// <inheritdoc />
public override IReadOnlyList<string> Tags => ["database", "migrations", "schema"];
/// <inheritdoc />
protected override async Task<DoctorCheckResult> 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<bool> 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));
}
}