Skip to content

Commit

Permalink
Merge pull request #5 from hummingbird-project/wait-for-migrations
Browse files Browse the repository at this point in the history
Add wait for migrations to complete function
  • Loading branch information
adam-fowler committed Mar 4, 2024
2 parents 20524ac + 79c0506 commit 8e7f196
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 11 deletions.
95 changes: 88 additions & 7 deletions Sources/HummingbirdPostgres/Migrations.swift
Expand Up @@ -16,36 +16,64 @@ import Logging
@_spi(ConnectionPool) import PostgresNIO

/// Database migration support
public final class HBPostgresMigrations {
public actor HBPostgresMigrations {
enum State {
case waiting([CheckedContinuation<Void, Error>])
case completed
case failed(Error)
}

var migrations: [HBPostgresMigration]
var reverts: [String: HBPostgresMigration]
var state: State

/// Initialize a HBPostgresMigrations object
public init() {
self.migrations = []
self.reverts = [:]
self.state = .waiting([])
}

/// Add migration to list of reverts, that can be applied
@MainActor
/// Add migration to list of migrations to be be applied
/// - Parameter migration: Migration to be applied
public func add(_ migration: HBPostgresMigration) {
self.migrations.append(migration)
}

/// Add migration to list of reverts, that can be applied
@MainActor
public func add(revert migration: HBPostgresMigration) {
/// - Parameter migration: Migration to be reverted if necessary
public func revert(_ migration: HBPostgresMigration) {
self.reverts[migration.name] = migration
}

/// Apply database migrations
///
/// This function compares the list of applied migrations and the list of desired migrations. If there
/// are migrations in the applied list that don't exist in the desired list then every migration after
/// the missing migration is reverted. Then every unapplied migration from the desired list is applied.
///
/// This means removing a single migration from the desired list will revert every migration after the
/// removed migation, changing the order will revert the moved migrations and any migration after.
///
/// As migrating can be a destructive process it is best to run this with `dryRun`` set to true by default
/// and only run it properly if an error is thrown to indicate a migration is required. But check the list
/// of reported migrations and reverts before doing this though.
///
/// - Parameters:
/// - client: Postgres client
/// - logger: Logger to use
/// - dryRun: Should migrations actually be applied, or should we just report what would be applied and reverted
@_spi(ConnectionPool)
@MainActor
public func apply(client: PostgresClient, groups: [HBMigrationGroup] = [], logger: Logger, dryRun: Bool) async throws {
try await self.migrate(client: client, migrations: self.migrations, groups: groups, logger: logger, dryRun: dryRun)
}

/// Revery database migrations
/// - Parameters:
/// - client: Postgres client
/// - logger: Logger to use
/// - dryRun: Should migrations actually be reverted, or should we just report what would be reverted
@_spi(ConnectionPool)
@MainActor
public func revert(client: PostgresClient, groups: [HBMigrationGroup] = [], logger: Logger, dryRun: Bool) async throws {
try await self.migrate(client: client, migrations: [], groups: groups, logger: logger, dryRun: dryRun)
}
Expand All @@ -57,6 +85,12 @@ public final class HBPostgresMigrations {
logger: Logger,
dryRun: Bool
) async throws {
switch self.state {
case .completed, .failed:
self.state = .waiting([])
case .waiting:
break
}
let repository = HBPostgresMigrationRepository(client: client)
do {
_ = try await repository.withContext(logger: logger) { context in
Expand Down Expand Up @@ -113,6 +147,53 @@ public final class HBPostgresMigrations {
throw HBPostgresMigrationError.requiresChanges
}
}
} catch {
self.setFailed(error)
throw error
}
self.setCompleted()
}

/// Report if the migration process has completed
public func waitUntilCompleted() async throws {
switch self.state {
case .waiting(var continuations):
return try await withCheckedThrowingContinuation { cont in
continuations.append(cont)
self.state = .waiting(continuations)
}
case .completed:
return
case .failed(let error):
throw error
}
}

func setCompleted() {
switch self.state {
case .waiting(let continuations):
for cont in continuations {
cont.resume()
}
self.state = .completed
case .completed:
break
case .failed:
preconditionFailure("Cannot set it has completed after having set it has failed")
}
}

func setFailed(_ error: Error) {
switch self.state {
case .waiting(let continuations):
for cont in continuations {
cont.resume(throwing: error)
}
self.state = .failed(error)
case .completed:
preconditionFailure("Cannot set it has failed after having set it has completed")
case .failed(let error):
self.state = .failed(error)
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions Tests/HummingbirdPostgresTests/MigrationTests.swift
Expand Up @@ -174,7 +174,7 @@ final class MigrationTests: XCTestCase {
try await self.testMigrations { migrations in
await migrations.add(TestMigration(name: "test1", order: order, applyOrder: 1))
await migrations.add(TestMigration(name: "test2", order: order, applyOrder: 2))
await migrations.add(revert: TestMigration(name: "test3", order: order, applyOrder: 3, revertOrder: 4))
await migrations.revert(TestMigration(name: "test3", order: order, applyOrder: 3, revertOrder: 4))
} verify: { migrations, client in
try await migrations.apply(client: client, groups: [.default], logger: self.logger, dryRun: false)
let migrations = try await getAll(client: client)
Expand All @@ -198,7 +198,7 @@ final class MigrationTests: XCTestCase {
await migrations.add(TestMigration(name: "test1", order: order, applyOrder: 1))
await migrations.add(TestMigration(name: "test2", order: order, applyOrder: 2))
await migrations.add(TestMigration(name: "test4", order: order, applyOrder: 5))
await migrations.add(revert: TestMigration(name: "test3", order: order, applyOrder: 3, revertOrder: 4))
await migrations.revert(TestMigration(name: "test3", order: order, applyOrder: 3, revertOrder: 4))
} verify: { migrations, client in
try await migrations.apply(client: client, groups: [.default], logger: self.logger, dryRun: false)
let migrations = try await getAll(client: client)
Expand Down Expand Up @@ -288,7 +288,7 @@ final class MigrationTests: XCTestCase {
// Remove migration from default group before the migration from the test group
try await self.testMigrations(groups: [.default, .test]) { migrations in
await migrations.add(TestMigration(name: "test1", order: order, applyOrder: 1, group: .default))
await migrations.add(revert: TestMigration(name: "test1_2", order: order, revertOrder: 4, group: .default))
await migrations.revert(TestMigration(name: "test1_2", order: order, revertOrder: 4, group: .default))
await migrations.add(TestMigration(name: "test2", order: order, applyOrder: 2, group: .test))
} verify: { migrations, client in
try await migrations.apply(client: client, groups: [.default, .test], logger: self.logger, dryRun: false)
Expand Down Expand Up @@ -323,7 +323,7 @@ final class MigrationTests: XCTestCase {
XCTAssertEqual(migrations[1], "test2")
}
try await self.testMigrations(groups: [.default, .test]) { migrations in
await migrations.add(revert: TestMigration(name: "test2", order: order, applyOrder: 2, revertOrder: 4, group: .test))
await migrations.revert(TestMigration(name: "test2", order: order, applyOrder: 2, revertOrder: 4, group: .test))
} verify: { _, _ in
}
order.expect(5)
Expand Down

0 comments on commit 8e7f196

Please sign in to comment.