Skip to content

Commit

Permalink
Merge pull request #6 from MikeAmputer/kravtsov/migration-fail-handling
Browse files Browse the repository at this point in the history
Migration fail handling
  • Loading branch information
MikeAmputer authored Oct 24, 2023
2 parents 3079d23 + b49d8ec commit 63b1b9a
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 39 deletions.
86 changes: 58 additions & 28 deletions src/ClickHouse.Facades/Migrations/ClickHouseMigrationFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@ namespace ClickHouse.Facades.Migrations;
internal sealed class ClickHouseMigrationFacade : ClickHouseFacade<ClickHouseMigrationContext>
{
private const string MigrationsTable = "db_migrations_history";
internal string DbName { get; set; } = string.Empty;

internal async Task EnsureMigrationsTableCreatedAsync(CancellationToken cancellationToken)
private readonly IClickHouseMigrationInstructions _migrationInstructions;
private readonly string _dbName;

public ClickHouseMigrationFacade(IClickHouseMigrationInstructions migrationInstructions)
{
ThrowIfDatabaseNotSet();
_migrationInstructions = migrationInstructions
?? throw new ArgumentNullException(nameof(migrationInstructions));

if (_migrationInstructions.DatabaseName.IsNullOrWhiteSpace())
{
throw new ArgumentException($"Migrations database name is null or white space.");
}

_dbName = _migrationInstructions.DatabaseName;
}

internal async Task EnsureMigrationsTableCreatedAsync(CancellationToken cancellationToken)
{
var builder = CreateTableSqlBuilder.Create
.IfNotExists()
.WithDatabase(DbName)
.WithDatabase(_dbName)
.WithTableName(MigrationsTable)
.AddColumn(builder => builder
.WithName("id")
Expand Down Expand Up @@ -42,11 +55,9 @@ internal async Task EnsureMigrationsTableCreatedAsync(CancellationToken cancella
internal async Task EnsureDatabaseCreatedAsync(
CancellationToken cancellationToken)
{
ThrowIfDatabaseNotSet();

var builder = CreateDatabaseSqlBuilder.Create
.IfNotExists()
.WithDbName(DbName)
.WithDbName(_dbName)
.WithEngine(builder => builder.WithEngine(ClickHouseDatabaseEngine.Atomic));

var statement = builder.BuildSql();
Expand All @@ -55,12 +66,10 @@ internal async Task EnsureDatabaseCreatedAsync(
}

private string GetAppliedMigrationsSql =>
$"select id, name from {DbName}.{MigrationsTable} final";
$"select id, name from {_dbName}.{MigrationsTable} final";

internal async Task<IEnumerable<AppliedMigration>> GetAppliedMigrationsAsync(CancellationToken cancellationToken)
{
ThrowIfDatabaseNotSet();

var migrations = await ExecuteQueryAsync(
GetAppliedMigrationsSql,
AppliedMigration.FromReader,
Expand All @@ -74,7 +83,6 @@ internal async Task<IEnumerable<AppliedMigration>> GetAppliedMigrationsAsync(Can

internal async Task ApplyMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken)
{
ThrowIfDatabaseNotSet();
ExceptionHelpers.ThrowIfNull(migration);

var migrationBuilder = ClickHouseMigrationBuilder.Create;
Expand All @@ -83,28 +91,39 @@ internal async Task ApplyMigrationAsync(ClickHouseMigration migration, Cancellat

cancellationToken.ThrowIfCancellationRequested();

foreach (var statement in migrationBuilder.Statements)
try
{
await ExecuteNonQueryAsync(statement, CancellationToken.None);
}

var addAppliedMigrationSql = string.Format(
AddAppliedMigrationSql,
new object[]
foreach (var statement in migrationBuilder.Statements)
{
$"{DbName}.{MigrationsTable}",
migration.Index,
migration.Name,
});
await ExecuteNonQueryAsync(statement, CancellationToken.None);
}

var addAppliedMigrationSql = string.Format(
AddAppliedMigrationSql,
new object[]
{
$"{_dbName}.{MigrationsTable}",
migration.Index,
migration.Name,
});

await ExecuteNonQueryAsync(addAppliedMigrationSql, CancellationToken.None);
}
catch (Exception migrationException)
{
var rolledBack = await TryRollbackMigrationAsync(migration);
var verb = rolledBack ? "has been" : "has not been";

await ExecuteNonQueryAsync(addAppliedMigrationSql, CancellationToken.None);
throw new AggregateException(
$"Failed to apply migration '{migration.Name}'. Migration {verb} rolled back.",
migrationException);
}
}

private const string AddRolledBackMigrationSql = "insert into {0} values ({1}, '{2}', 1)";

internal async Task RollbackMigrationAsync(ClickHouseMigration migration, CancellationToken cancellationToken)
{
ThrowIfDatabaseNotSet();
ExceptionHelpers.ThrowIfNull(migration);

var migrationBuilder = ClickHouseMigrationBuilder.Create;
Expand All @@ -122,19 +141,30 @@ internal async Task RollbackMigrationAsync(ClickHouseMigration migration, Cancel
AddRolledBackMigrationSql,
new object[]
{
$"{DbName}.{MigrationsTable}",
$"{_dbName}.{MigrationsTable}",
migration.Index,
migration.Name,
});

await ExecuteNonQueryAsync(addAppliedMigrationSql, CancellationToken.None);
}

private void ThrowIfDatabaseNotSet()
private async Task<bool> TryRollbackMigrationAsync(ClickHouseMigration migration)
{
if (DbName.IsNullOrWhiteSpace())
if (!_migrationInstructions.RollbackOnMigrationFail)
{
return false;
}

try
{
await RollbackMigrationAsync(migration, CancellationToken.None);

return true;
}
catch (Exception)
{
throw new InvalidOperationException($"{nameof(DbName)} is not set.");
return false;
}
}
}
11 changes: 0 additions & 11 deletions src/ClickHouse.Facades/Migrations/ClickHouseMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ internal class ClickHouseMigrator : IClickHouseMigrator
private readonly IClickHouseContextFactory<ClickHouseMigrationContext> _migrationContextFactory;
private readonly IClickHouseMigrationsLocator _migrationsLocator;

private readonly string _migrationsDatabase;

public ClickHouseMigrator(
IClickHouseContextFactory<ClickHouseMigrationContext> migrationContextFactory,
IClickHouseMigrationsLocator migrationsLocator,
Expand All @@ -21,21 +19,13 @@ public ClickHouseMigrator(
?? throw new ArgumentNullException(nameof(migrationsLocator));

ExceptionHelpers.ThrowIfNull(instructions);

_migrationsDatabase = instructions.DatabaseName;

if (_migrationsDatabase.IsNullOrWhiteSpace())
{
throw new ArgumentException($"Migrations database name is null or white space.");
}
}

public async Task ApplyMigrationsAsync(CancellationToken cancellationToken = default)
{
await using var context = _migrationContextFactory.CreateContext();

var facade = context.GetFacade<ClickHouseMigrationFacade>();
facade.DbName = _migrationsDatabase;

await EnsureDatabaseCreated(context, cancellationToken);
await facade.EnsureMigrationsTableCreatedAsync(cancellationToken);
Expand All @@ -58,7 +48,6 @@ public async Task RollbackAsync(ulong targetMigrationId, CancellationToken cance
await using var context = _migrationContextFactory.CreateContext();

var facade = context.GetFacade<ClickHouseMigrationFacade>();
facade.DbName = _migrationsDatabase;

var migrationsResolver = new MigrationsResolver(
await facade.GetAppliedMigrationsAsync(cancellationToken),
Expand Down
2 changes: 2 additions & 0 deletions src/ClickHouse.Facades/Migrations/IClickHouseMigrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
public interface IClickHouseMigrator
{
public Task ApplyMigrationsAsync(CancellationToken cancellationToken = default);

public Task RollbackAsync(ulong targetMigrationId, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface IClickHouseMigrationInstructions
/// </summary>
string DatabaseName => GetConnectionString().GetConnectionStringParameters()["database"]
?? throw new InvalidOperationException("Unable to get 'database' parameter from connection string.");

bool RollbackOnMigrationFail => false;
}

0 comments on commit 63b1b9a

Please sign in to comment.