using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using StellaOps.Infrastructure.Postgres.Migrations;
namespace StellaOps.Cli.Services;
///
/// Helper for running, verifying, and querying PostgreSQL migrations from the CLI.
///
internal sealed class MigrationCommandService
{
private readonly IConfiguration _configuration;
private readonly ILoggerFactory _loggerFactory;
public MigrationCommandService(IConfiguration configuration, ILoggerFactory loggerFactory)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public Task RunAsync(
MigrationModuleInfo module,
string? connectionOverride,
MigrationCategory? category,
bool dryRun,
int? timeoutSeconds,
CancellationToken cancellationToken)
{
var connectionString = ResolveConnectionString(module, connectionOverride);
var runner = CreateRunner(module, connectionString);
var options = new MigrationRunOptions
{
CategoryFilter = category,
DryRun = dryRun,
TimeoutSeconds = timeoutSeconds.GetValueOrDefault(300),
ValidateChecksums = true,
FailOnChecksumMismatch = true
};
return runner.RunFromAssemblyAsync(module.MigrationsAssembly, module.ResourcePrefix, options, cancellationToken);
}
public async Task GetStatusAsync(
MigrationModuleInfo module,
string? connectionOverride,
CancellationToken cancellationToken)
{
var connectionString = ResolveConnectionString(module, connectionOverride);
var logger = _loggerFactory.CreateLogger($"migrationstatus.{module.Name}");
var statusService = new MigrationStatusService(
connectionString,
module.SchemaName,
module.Name,
module.MigrationsAssembly,
logger);
return await statusService.GetStatusAsync(cancellationToken).ConfigureAwait(false);
}
public Task> VerifyAsync(
MigrationModuleInfo module,
string? connectionOverride,
CancellationToken cancellationToken)
{
var connectionString = ResolveConnectionString(module, connectionOverride);
var runner = CreateRunner(module, connectionString);
return runner.ValidateChecksumsAsync(module.MigrationsAssembly, module.ResourcePrefix, cancellationToken);
}
private MigrationRunner CreateRunner(MigrationModuleInfo module, string connectionString) =>
new(connectionString, module.SchemaName, module.Name, _loggerFactory.CreateLogger($"migration.{module.Name}"));
private string ResolveConnectionString(MigrationModuleInfo module, string? connectionOverride)
{
if (!string.IsNullOrWhiteSpace(connectionOverride))
{
return connectionOverride;
}
var envCandidates = new[]
{
$"STELLAOPS_POSTGRES_{module.Name.ToUpperInvariant()}_CONNECTION",
$"STELLAOPS_POSTGRES_{module.SchemaName.ToUpperInvariant()}_CONNECTION",
"STELLAOPS_POSTGRES_CONNECTION",
"STELLAOPS_DB_CONNECTION"
};
foreach (var key in envCandidates)
{
var value = Environment.GetEnvironmentVariable(key);
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
var configCandidates = new[]
{
$"StellaOps:Database:{module.Name}:ConnectionString",
$"Database:{module.Name}:ConnectionString",
$"StellaOps:Postgres:ConnectionString",
$"Postgres:ConnectionString",
"Database:ConnectionString"
};
foreach (var key in configCandidates)
{
var value = _configuration[key];
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
throw new InvalidOperationException(
$"No PostgreSQL connection string found for module '{module.Name}'. " +
"Provide --connection or set STELLAOPS_POSTGRES_CONNECTION.");
}
}