pg-migrate-1.1.0.0: src/Database/PostgreSQL/Migrate/Repair.hs
module Database.PostgreSQL.Migrate.Repair
( repairMigration,
)
where
import Data.Int (Int32, Int64)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text qualified as Text
import Data.Version (showVersion)
import Database.PostgreSQL.Migrate.Ledger
import Database.PostgreSQL.Migrate.Ledger.Migrations
import Database.PostgreSQL.Migrate.Ledger.Sql
import Database.PostgreSQL.Migrate.Ledger.Types
import Database.PostgreSQL.Migrate.Plan (planDescription)
import Database.PostgreSQL.Migrate.Repair.Types
import Database.PostgreSQL.Migrate.Runner
import Database.PostgreSQL.Migrate.Runner.Types
import Database.PostgreSQL.Migrate.Types
import Hasql.Connection qualified as Connection
import Hasql.Session qualified as Session
import Hasql.Statement qualified as Statement
import Hasql.Transaction qualified as Transaction
import Hasql.Transaction.Sessions qualified as Transaction.Sessions
import Paths_pg_migrate qualified as Package
import PgMigrate.Prelude
-- | Repair one verified nontransactional row under the normal runner lifecycle.
repairMigration ::
RunOptions ->
ConnectionProvider ->
MigrationPlan ->
RepairRequest ->
IO (Either RepairError RepairReport)
repairMigration options provider plan request = do
(result, observedCleanupIssues) <-
withRunLifecycle options provider $ \connection ->
repairLocked options connection plan request >>= \case
Left (RepairRunnerError migrationError) -> pure (Left migrationError)
repairResult -> pure (Right repairResult)
pure $ case result of
Left migrationError -> Left (RepairRunnerError (attachCleanup observedCleanupIssues migrationError))
Right (Left repairError) -> Left repairError
Right
( Right
RepairReport
{ repairedMigration,
operation,
oldStatus,
newStatus
}
) ->
Right RepairReport {repairedMigration, operation, oldStatus, newStatus, cleanupIssues = observedCleanupIssues}
where
attachCleanup cleanup migrationError =
case NonEmpty.nonEmpty cleanup of
Nothing -> migrationError
Just issues -> CleanupFailed migrationError issues
repairLocked ::
RunOptions ->
Connection.Connection ->
MigrationPlan ->
RepairRequest ->
IO (Either RepairError RepairReport)
repairLocked options connection plan request = do
initialized <-
runSession
connection
(initializeOrUpgradeLedger (runLedgerConfig options) repairRunnerVersion)
case initialized of
Left migrationError -> pure (Left (RepairRunnerError migrationError))
Right (Left ledgerError) ->
pure (Left (RepairRunnerError (LedgerInitializationFailed ledgerError)))
Right (Right ()) -> do
loaded <- runSession connection (loadLedger (runLedgerConfig options))
case loaded of
Left migrationError -> pure (Left (RepairRunnerError migrationError))
Right snapshot -> repairVerified options connection plan request snapshot
repairVerified ::
RunOptions ->
Connection.Connection ->
MigrationPlan ->
RepairRequest ->
LedgerSnapshot ->
IO (Either RepairError RepairReport)
repairVerified options connection plan request snapshot =
case find ((== targetId) . storedMigrationId) (storedMigrations snapshot) of
Nothing -> pure (Left (RepairTargetMissing targetId))
Just stored ->
case findPlannedMigration targetId plan of
Nothing -> pure (Left (RepairTargetNotInPlan targetId))
Just (position, migration) ->
case validateRepairTarget targetId position migration stored verification of
Left repairError -> pure (Left repairError)
Right oldStatus ->
case repairOperation request of
MarkApplied -> markApplied options connection request oldStatus
Retry -> retry options connection request oldStatus position migration
where
targetId = repairMigrationId request
verification =
comparePlanWithLedger
(runUnknownMigrationsPolicy options)
(planDescription plan)
(storedMigrations snapshot)
validateRepairTarget ::
MigrationId ->
Int32 ->
Migration ->
StoredMigration ->
VerificationReport ->
Either RepairError MigrationStatus
validateRepairTarget targetId expectedPosition migration stored verification = do
let StoredMigration
{ position = storedPosition,
checksum = storedChecksum,
kind = storedKind,
transactionMode = storedMode,
status = storedStatus
} = stored
if storedMode /= NonTransactional || migrationModeOf migration /= NonTransactional
then Left (RepairTargetTransactional targetId)
else Right ()
if storedPosition /= fromIntegral expectedPosition
|| storedChecksum /= migrationChecksumOf migration
|| storedKind /= migrationKindOf migration
|| storedMode /= migrationModeOf migration
then Left (RepairTargetMetadataMismatch targetId)
else Right ()
let unrelatedIssues = filter (not . targetStatusIssue targetId) (verificationIssues verification)
case unrelatedIssues of
[] -> Right ()
_ -> Left (RepairBlockedByVerification (withVerificationIssues unrelatedIssues verification))
case storedStatus of
Applied -> Left (RepairTargetAlreadyApplied targetId)
Running -> Right ()
Failed -> Right ()
Right storedStatus
markApplied ::
RunOptions ->
Connection.Connection ->
RepairRequest ->
MigrationStatus ->
IO (Either RepairError RepairReport)
markApplied options connection request oldStatus = do
transitioned <-
runRepairTransaction
connection
(runLedgerConfig options)
(repairLedgerRow request oldStatus Applied)
prepareMarkAppliedStatement
pure $ case transitioned of
Left migrationError -> Left (RepairRunnerError migrationError)
Right False -> Left (RepairTransitionFailed (repairMigrationId request))
Right True ->
Right
RepairReport
{ repairedMigration = repairMigrationId request,
operation = MarkApplied,
oldStatus,
newStatus = Applied,
cleanupIssues = []
}
retry ::
RunOptions ->
Connection.Connection ->
RepairRequest ->
MigrationStatus ->
Int32 ->
Migration ->
IO (Either RepairError RepairReport)
retry options connection request oldStatus position migration = do
started <- invokeEvent options (MigrationStarted (repairMigrationId request))
case started of
Left migrationError -> pure (Left (RepairRunnerError migrationError))
Right () -> do
prepared <-
runRepairTransaction
connection
(runLedgerConfig options)
(repairLedgerRow request oldStatus Running)
prepareRetryStatement
case prepared of
Left migrationError -> pure (Left (RepairRunnerError migrationError))
Right False -> pure (Left (RepairTransitionFailed (repairMigrationId request)))
Right True -> do
executed <-
resumeNonTransactionalMigration
options
connection
(repairMigrationId request)
position
migration
pure $ case executed of
Left migrationError -> Left (RepairRunnerError migrationError)
Right _ ->
Right
RepairReport
{ repairedMigration = repairMigrationId request,
operation = Retry,
oldStatus,
newStatus = Applied,
cleanupIssues = []
}
runRepairTransaction ::
Connection.Connection ->
LedgerConfig ->
RepairLedgerRow ->
(LedgerConfig -> Statement.Statement RepairLedgerRow Int64) ->
IO (Either MigrationError Bool)
runRepairTransaction connection config repairRow transitionStatement = do
result <-
Connection.use
connection
( Transaction.Sessions.transactionNoRetry
Transaction.Sessions.ReadCommitted
Transaction.Sessions.Write
$ do
Transaction.statement repairRow (insertRepairAuditStatement config)
affected <- Transaction.statement repairRow (transitionStatement config)
when (affected /= 1) Transaction.condemn
pure affected
)
pure $ first DatabaseSessionFailed ((== 1) <$> result)
repairLedgerRow ::
RepairRequest ->
MigrationStatus ->
MigrationStatus ->
RepairLedgerRow
repairLedgerRow request oldStatus newStatus =
RepairLedgerRow
{ repairLedgerMigrationId = repairMigrationId request,
repairLedgerOperation = case repairOperation request of MarkApplied -> "mark-applied"; Retry -> "retry",
repairLedgerOldStatus = oldStatus,
repairLedgerNewStatus = newStatus,
repairLedgerReason = repairReason request,
repairLedgerRunnerVersion = repairRunnerVersion
}
findPlannedMigration :: MigrationId -> MigrationPlan -> Maybe (Int32, Migration)
findPlannedMigration targetId plan =
find ((== targetId) . plannedId) (concatMap componentMigrations (toList (planComponentsOf plan)))
>>= \(_, position, migration) -> Just (position, migration)
where
componentMigrations component =
zipWith
(\position migration -> (MigrationId (componentNameOf component) (migrationNameOf migration), position, migration))
[1 ..]
(toList (componentMigrationsOf component))
plannedId (identifier, _, _) = identifier
targetStatusIssue :: MigrationId -> VerificationIssue -> Bool
targetStatusIssue targetId = \case
StoredMigrationRunning identifier -> identifier == targetId
StoredMigrationFailed identifier -> identifier == targetId
_ -> False
verificationIssues :: VerificationReport -> [VerificationIssue]
verificationIssues VerificationReport {issues} = issues
withVerificationIssues :: [VerificationIssue] -> VerificationReport -> VerificationReport
withVerificationIssues issues VerificationReport {appliedMigrations, pendingMigrations, unknownMigrations} =
VerificationReport {issues, appliedMigrations, pendingMigrations, unknownMigrations}
runSession ::
Connection.Connection ->
Session.Session value ->
IO (Either MigrationError value)
runSession connection session = first DatabaseSessionFailed <$> Connection.use connection session
repairRunnerVersion :: Text
repairRunnerVersion = Text.pack (showVersion Package.version)