Files
git.stella-ops.org/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/IMigrationRunner.cs
master 75f6942769
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
Add integration tests for migration categories and execution
- 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.
2025-12-04 19:10:54 +02:00

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);