Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
- Implemented MigrationCategoryTests to validate migration categorization for startup, release, seed, and data migrations. - Added tests for edge cases, including null, empty, and whitespace migration names. - Created StartupMigrationHostTests to verify the behavior of the migration host with real PostgreSQL instances using Testcontainers. - Included tests for migration execution, schema creation, and handling of pending release migrations. - Added SQL migration files for testing: creating a test table, adding a column, a release migration, and seeding data.
182 lines
5.7 KiB
C#
182 lines
5.7 KiB
C#
using System.Reflection;
|
|
|
|
namespace StellaOps.Infrastructure.Postgres.Migrations;
|
|
|
|
/// <summary>
|
|
/// Interface for running database migrations.
|
|
/// </summary>
|
|
public interface IMigrationRunner
|
|
{
|
|
/// <summary>
|
|
/// Gets the schema name for this migration runner.
|
|
/// </summary>
|
|
string SchemaName { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the module name for this migration runner.
|
|
/// </summary>
|
|
string ModuleName { get; }
|
|
|
|
/// <summary>
|
|
/// Runs pending migrations from the specified path.
|
|
/// </summary>
|
|
/// <param name="migrationsPath">Path to directory containing SQL migration files.</param>
|
|
/// <param name="options">Migration execution options.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Result of migration execution.</returns>
|
|
Task<MigrationResult> RunAsync(
|
|
string migrationsPath,
|
|
MigrationRunOptions? options = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Runs pending migrations from embedded resources in an assembly.
|
|
/// </summary>
|
|
/// <param name="assembly">Assembly containing embedded migration resources.</param>
|
|
/// <param name="resourcePrefix">Optional prefix to filter resources.</param>
|
|
/// <param name="options">Migration execution options.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>Result of migration execution.</returns>
|
|
Task<MigrationResult> RunFromAssemblyAsync(
|
|
Assembly assembly,
|
|
string? resourcePrefix = null,
|
|
MigrationRunOptions? options = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets the current migration version (latest applied migration).
|
|
/// </summary>
|
|
Task<string?> GetCurrentVersionAsync(CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Gets all applied migrations.
|
|
/// </summary>
|
|
Task<IReadOnlyList<MigrationInfo>> GetAppliedMigrationInfoAsync(CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Validates checksums of applied migrations against source files.
|
|
/// </summary>
|
|
/// <param name="assembly">Assembly containing embedded migration resources.</param>
|
|
/// <param name="resourcePrefix">Optional prefix to filter resources.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>List of checksum validation errors, empty if all valid.</returns>
|
|
Task<IReadOnlyList<string>> ValidateChecksumsAsync(
|
|
Assembly assembly,
|
|
string? resourcePrefix = null,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Options for migration execution.
|
|
/// </summary>
|
|
public sealed class MigrationRunOptions
|
|
{
|
|
/// <summary>
|
|
/// Filter migrations by category. If null, all categories are included.
|
|
/// </summary>
|
|
public MigrationCategory? CategoryFilter { get; set; }
|
|
|
|
/// <summary>
|
|
/// If true, only show what would be executed without applying.
|
|
/// </summary>
|
|
public bool DryRun { get; set; }
|
|
|
|
/// <summary>
|
|
/// Timeout in seconds for individual migration execution. Default: 300.
|
|
/// </summary>
|
|
public int TimeoutSeconds { get; set; } = 300;
|
|
|
|
/// <summary>
|
|
/// If true, validate checksums before applying new migrations.
|
|
/// </summary>
|
|
public bool ValidateChecksums { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// If true, fail if checksum validation errors are found.
|
|
/// </summary>
|
|
public bool FailOnChecksumMismatch { get; set; } = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of a migration execution.
|
|
/// </summary>
|
|
public sealed class MigrationResult
|
|
{
|
|
/// <summary>
|
|
/// Whether the migration run was successful.
|
|
/// </summary>
|
|
public bool Success { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of migrations applied.
|
|
/// </summary>
|
|
public int AppliedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of migrations skipped (already applied).
|
|
/// </summary>
|
|
public int SkippedCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Number of migrations filtered out by category.
|
|
/// </summary>
|
|
public int FilteredCount { get; init; }
|
|
|
|
/// <summary>
|
|
/// Total duration in milliseconds.
|
|
/// </summary>
|
|
public long DurationMs { get; init; }
|
|
|
|
/// <summary>
|
|
/// Details of applied migrations.
|
|
/// </summary>
|
|
public IReadOnlyList<AppliedMigrationDetail> AppliedMigrations { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Checksum validation errors, if any.
|
|
/// </summary>
|
|
public IReadOnlyList<string> ChecksumErrors { get; init; } = [];
|
|
|
|
/// <summary>
|
|
/// Error message if migration failed.
|
|
/// </summary>
|
|
public string? ErrorMessage { get; init; }
|
|
|
|
/// <summary>
|
|
/// Creates a successful result.
|
|
/// </summary>
|
|
public static MigrationResult Successful(
|
|
int appliedCount,
|
|
int skippedCount,
|
|
int filteredCount,
|
|
long durationMs,
|
|
IReadOnlyList<AppliedMigrationDetail> appliedMigrations) => new()
|
|
{
|
|
Success = true,
|
|
AppliedCount = appliedCount,
|
|
SkippedCount = skippedCount,
|
|
FilteredCount = filteredCount,
|
|
DurationMs = durationMs,
|
|
AppliedMigrations = appliedMigrations
|
|
};
|
|
|
|
/// <summary>
|
|
/// Creates a failed result.
|
|
/// </summary>
|
|
public static MigrationResult Failed(string errorMessage, IReadOnlyList<string>? checksumErrors = null) => new()
|
|
{
|
|
Success = false,
|
|
ErrorMessage = errorMessage,
|
|
ChecksumErrors = checksumErrors ?? []
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Details of an applied migration.
|
|
/// </summary>
|
|
public sealed record AppliedMigrationDetail(
|
|
string Name,
|
|
MigrationCategory Category,
|
|
long DurationMs,
|
|
bool WasDryRun);
|