packages feed

pg-migrate-cli-1.0.0.0: src/Database/PostgreSQL/Migrate/CLI/Handler.hs

module Database.PostgreSQL.Migrate.CLI.Handler
  ( CliEnvironment,
    cliEnvironment,
    cliEnvironmentWithConnectionProvider,
    runMigrationCommand,
  )
where

import Data.List.NonEmpty qualified as NonEmpty
import Data.Text.Encoding qualified as Text.Encoding
import Database.PostgreSQL.Migrate
  ( ComponentName,
    Confirmation,
    ConnectionProvider,
    MigrationId,
    MigrationPlan,
    RepairOperation,
    RunOptions,
    StatusReport (..),
    StoredMigration (..),
    VerificationReport (..),
    connectionProviderFromSettings,
    migrationFingerprint,
    migrationStatusWith,
    repairMigration,
    repairRequest,
    runMigrationPlanWith,
    verifyMigrationPlanWith,
    withLockWait,
    withStatementTimeout,
  )
import Database.PostgreSQL.Migrate.CLI.Outcome
import Database.PostgreSQL.Migrate.CLI.Types
import Database.PostgreSQL.Migrate.Embed
  ( AuthoringError (..),
    checkMigrationManifest,
    newMigration,
    newMigrationOptions,
  )
import Database.PostgreSQL.Migrate.Internal
  ( ComponentDescription (..),
    MigrationDescription (..),
    PlanDescription (..),
    migrationIdComponent,
    migrationIdName,
    planDescription,
  )
import Hasql.Connection.Settings qualified as Settings
import PgMigrate.CLI.Prelude

data CliConnection
  = SettingsConnection !Settings.Settings
  | ProviderConnection !ConnectionProvider

-- | Application-supplied plan, runner defaults, and connection ownership boundary.
data CliEnvironment = CliEnvironment
  { defaultConnection :: !CliConnection,
    migrationPlan :: !MigrationPlan,
    runnerOptions :: !RunOptions
  }

-- | Construct a CLI environment from concrete Hasql settings.
cliEnvironment :: Settings.Settings -> MigrationPlan -> RunOptions -> CliEnvironment
cliEnvironment settings = CliEnvironment (SettingsConnection settings)

-- | Construct a CLI environment from an application connection provider.
cliEnvironmentWithConnectionProvider ::
  ConnectionProvider ->
  MigrationPlan ->
  RunOptions ->
  CliEnvironment
cliEnvironmentWithConnectionProvider provider = CliEnvironment (ProviderConnection provider)

-- | Dispatch a parsed command without writing streams or exiting the process.
runMigrationCommand :: CliEnvironment -> MigrationCommand -> IO CliOutcome
runMigrationCommand environment commandValue =
  case commandValue of
    Plan PlanOptions {inspection} ->
      pure
        ( success
            "plan"
            (PlanPayload (filterPlan inspection (planDescription (migrationPlan environment))))
        )
    List ListOptions {inspection} ->
      pure
        ( success
            "list"
            ( ListPayload
                ( filterMigrations
                    inspection
                    (flattenPlanDescription (planDescription (migrationPlan environment)))
                )
            )
        )
    Check CheckOptions {manifestPath} -> runCheck manifestPath
    Status StatusOptions {inspection, connection} -> runStatus environment inspection connection
    Verify VerifyOptions {inspection, connection} -> runVerify environment inspection connection
    Up UpOptions {connection, execution} -> runUp environment connection execution
    Repair RepairOptions {target, operation, reason, confirmation, connection, execution} ->
      runRepair environment connection execution target operation reason confirmation
    New NewOptions {manifestPath, description, requestedName} ->
      runNew manifestPath description requestedName

runCheck :: FilePath -> IO CliOutcome
runCheck manifestPath = do
  checked <- checkMigrationManifest manifestPath
  pure $ case checked of
    Left manifestError -> failure "check" ExitUsageFailed (CliManifestError manifestError)
    Right entries ->
      success
        "check"
        ( CheckPayload
            ((\(file, bytes) -> CheckedMigration file (migrationFingerprint bytes)) <$> entries)
        )

runStatus :: CliEnvironment -> InspectionOptions -> ConnectionOptions -> IO CliOutcome
runStatus environment inspection connection = do
  result <-
    migrationStatusWith
      (runnerOptions environment)
      (selectProvider environment connection)
      (migrationPlan environment)
  pure $ case result of
    Left migrationError -> failure "status" ExitExecutionFailed (CliMigrationError migrationError)
    Right report -> success "status" (StatusPayload (filterStatus inspection report))

runVerify :: CliEnvironment -> InspectionOptions -> ConnectionOptions -> IO CliOutcome
runVerify environment inspection connection = do
  result <-
    verifyMigrationPlanWith
      (runnerOptions environment)
      (selectProvider environment connection)
      (migrationPlan environment)
  pure $ case result of
    Left migrationError -> failure "verify" ExitExecutionFailed (CliMigrationError migrationError)
    Right report ->
      CliOutcome
        { command = "verify",
          exitClass = verificationExitClass report,
          payload = Right (VerifyPayload (filterVerification inspection report))
        }

runUp :: CliEnvironment -> ConnectionOptions -> ExecutionOptions -> IO CliOutcome
runUp environment connection execution = do
  result <-
    runMigrationPlanWith
      (applyExecution execution (runnerOptions environment))
      (selectProvider environment connection)
      (migrationPlan environment)
  pure $ case result of
    Left migrationError -> failure "up" ExitExecutionFailed (CliMigrationError migrationError)
    Right report -> success "up" (UpPayload report)

runRepair ::
  CliEnvironment ->
  ConnectionOptions ->
  ExecutionOptions ->
  MigrationId ->
  RepairOperation ->
  Text ->
  Confirmation ->
  IO CliOutcome
runRepair environment connection execution target operation reason confirmation =
  case repairRequest target operation reason confirmation of
    Left definitionError ->
      pure (failure "repair" ExitUsageFailed (CliRepairDefinitionError definitionError))
    Right request -> do
      result <-
        repairMigration
          (applyExecution execution (runnerOptions environment))
          (selectProvider environment connection)
          (migrationPlan environment)
          request
      pure $ case result of
        Left repairError -> failure "repair" ExitExecutionFailed (CliRepairError repairError)
        Right report -> success "repair" (RepairPayload report)

runNew :: FilePath -> Text -> Maybe FilePath -> IO CliOutcome
runNew manifestPath description requestedName =
  case newMigrationOptions manifestPath requestedName initialSql of
    Left authoringError ->
      pure (failure "new" ExitUsageFailed (CliAuthoringError authoringError))
    Right options -> do
      result <- newMigration options
      pure $ case result of
        Left authoringError ->
          failure "new" (authoringExitClass authoringError) (CliAuthoringError authoringError)
        Right path -> success "new" (NewPayload path)
  where
    initialSql = Text.Encoding.encodeUtf8 ("-- " <> description <> "\n\n")

selectProvider :: CliEnvironment -> ConnectionOptions -> ConnectionProvider
selectProvider CliEnvironment {defaultConnection} ConnectionOptions {databaseSettings} =
  case databaseSettings of
    Just settings -> connectionProviderFromSettings settings
    Nothing ->
      case defaultConnection of
        SettingsConnection settings -> connectionProviderFromSettings settings
        ProviderConnection provider -> provider

applyExecution :: ExecutionOptions -> RunOptions -> RunOptions
applyExecution ExecutionOptions {lockWait, statementTimeout} =
  withStatementTimeout statementTimeout . withLockWait lockWait

verificationExitClass :: VerificationReport -> ExitClass
verificationExitClass VerificationReport {issues}
  | null issues = ExitSuccess
  | otherwise = ExitVerificationFailed

authoringExitClass :: AuthoringError -> ExitClass
authoringExitClass authoringError =
  case authoringError of
    AuthoringIoError {} -> ExitExecutionFailed
    AuthoringCleanupError {} -> ExitExecutionFailed
    _ -> ExitUsageFailed

success :: Text -> CliPayload -> CliOutcome
success command payload = CliOutcome {command, exitClass = ExitSuccess, payload = Right payload}

failure :: Text -> ExitClass -> CliError -> CliOutcome
failure command exitClass cliError = CliOutcome {command, exitClass, payload = Left cliError}

flattenPlanDescription :: PlanDescription -> [MigrationDescription]
flattenPlanDescription (PlanDescription components) =
  concatMap (toList . migrations) (toList components)

filterPlan :: InspectionOptions -> PlanDescription -> [ComponentDescription]
filterPlan inspection (PlanDescription components) =
  foldr filterComponent [] (toList components)
  where
    filterComponent componentDescription@ComponentDescription {name, migrations} remaining
      | not (matchesComponent inspection name) = remaining
      | otherwise =
          case NonEmpty.nonEmpty (filter (matchesMigrationDescription inspection) (toList migrations)) of
            Nothing -> remaining
            Just filteredMigrations ->
              componentDescription {migrations = filteredMigrations} : remaining

filterMigrations :: InspectionOptions -> [MigrationDescription] -> [MigrationDescription]
filterMigrations inspection = filter (matchesMigrationDescription inspection)

filterStatus :: InspectionOptions -> StatusReport -> StatusReport
filterStatus inspection (StatusReport issues applied pending unknown) =
  StatusReport
    issues
    (filter (matchesMigrationId inspection) applied)
    (filter (matchesMigrationId inspection) pending)
    (filter (matchesMigrationId inspection . storedMigrationId) unknown)

filterVerification :: InspectionOptions -> VerificationReport -> VerificationReport
filterVerification inspection (VerificationReport issues applied pending unknown) =
  VerificationReport
    issues
    (filter (matchesMigrationId inspection) applied)
    (filter (matchesMigrationId inspection) pending)
    (filter (matchesMigrationId inspection . storedMigrationId) unknown)

matchesMigrationDescription :: InspectionOptions -> MigrationDescription -> Bool
matchesMigrationDescription inspection MigrationDescription {migrationId} =
  matchesMigrationId inspection migrationId

matchesMigrationId :: InspectionOptions -> MigrationId -> Bool
matchesMigrationId InspectionOptions {component, migration} identifier =
  maybe True (== migrationIdComponent identifier) component
    && maybe True (== migrationIdName identifier) migration

matchesComponent :: InspectionOptions -> ComponentName -> Bool
matchesComponent InspectionOptions {component} candidate = maybe True (== candidate) component