up
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Npgsql;
|
||||
|
||||
@@ -110,6 +111,85 @@ public sealed class MigrationRunner
|
||||
return appliedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs all 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 (e.g., "Migrations").</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Number of migrations applied.</returns>
|
||||
public async Task<int> RunFromAssemblyAsync(
|
||||
Assembly assembly,
|
||||
string? resourcePrefix = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(assembly);
|
||||
|
||||
var resourceNames = assembly.GetManifestResourceNames()
|
||||
.Where(name => name.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(name => string.IsNullOrEmpty(resourcePrefix) || name.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(name => name)
|
||||
.ToList();
|
||||
|
||||
if (resourceNames.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("No embedded migration resources found in assembly {Assembly} for module {Module}.",
|
||||
assembly.GetName().Name, _moduleName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
await using var connection = new NpgsqlConnection(_connectionString);
|
||||
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Ensure schema exists
|
||||
await EnsureSchemaAsync(connection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Ensure migrations table exists
|
||||
await EnsureMigrationsTableAsync(connection, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Get applied migrations
|
||||
var appliedMigrations = await GetAppliedMigrationsAsync(connection, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var appliedCount = 0;
|
||||
|
||||
foreach (var resourceName in resourceNames)
|
||||
{
|
||||
// Extract just the filename from the resource name
|
||||
var fileName = ExtractMigrationFileName(resourceName);
|
||||
|
||||
if (appliedMigrations.Contains(fileName))
|
||||
{
|
||||
_logger.LogDebug("Migration {Migration} already applied for module {Module}.",
|
||||
fileName, _moduleName);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Applying migration {Migration} for module {Module}...",
|
||||
fileName, _moduleName);
|
||||
|
||||
await ApplyMigrationFromResourceAsync(connection, assembly, resourceName, fileName, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
appliedCount++;
|
||||
|
||||
_logger.LogInformation("Migration {Migration} applied successfully for module {Module}.",
|
||||
fileName, _moduleName);
|
||||
}
|
||||
|
||||
if (appliedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("Applied {Count} embedded migration(s) for module {Module}.",
|
||||
appliedCount, _moduleName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Database is up to date for module {Module}.", _moduleName);
|
||||
}
|
||||
|
||||
return appliedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current migration version (latest applied migration).
|
||||
/// </summary>
|
||||
@@ -270,6 +350,63 @@ public sealed class MigrationRunner
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyMigrationFromResourceAsync(
|
||||
NpgsqlConnection connection,
|
||||
Assembly assembly,
|
||||
string resourceName,
|
||||
string fileName,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException($"Could not load embedded resource: {resourceName}");
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var sql = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
var checksum = ComputeChecksum(sql);
|
||||
|
||||
await using var transaction = await connection.BeginTransactionAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
// Run migration SQL
|
||||
await using (var migrationCommand = new NpgsqlCommand(sql, connection, transaction))
|
||||
{
|
||||
migrationCommand.CommandTimeout = 300; // 5 minute timeout for migrations
|
||||
await migrationCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Record migration
|
||||
await using (var recordCommand = new NpgsqlCommand(
|
||||
$"""
|
||||
INSERT INTO {_schemaName}.schema_migrations (migration_name, checksum)
|
||||
VALUES (@name, @checksum);
|
||||
""",
|
||||
connection,
|
||||
transaction))
|
||||
{
|
||||
recordCommand.Parameters.AddWithValue("name", fileName);
|
||||
recordCommand.Parameters.AddWithValue("checksum", checksum);
|
||||
await recordCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractMigrationFileName(string resourceName)
|
||||
{
|
||||
// Resource names use the LogicalName from .csproj which is just the filename
|
||||
// e.g., "001_initial.sql" or might have path prefix like "Migrations/001_initial.sql"
|
||||
var lastSlash = resourceName.LastIndexOf('/');
|
||||
return lastSlash >= 0 ? resourceName[(lastSlash + 1)..] : resourceName;
|
||||
}
|
||||
|
||||
private static string ComputeChecksum(string content)
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
|
||||
|
||||
Reference in New Issue
Block a user