112 lines
4.2 KiB
C#
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));
|
|
}
|
|
}
|