packages feed

pg-migrate-cli-1.1.0.0: src/Database/PostgreSQL/Migrate/CLI/Text.hs

module Database.PostgreSQL.Migrate.CLI.Text
  ( renderMigrationCommandText,
  )
where

import Data.List.NonEmpty qualified as NonEmpty
import Data.Set qualified as Set
import Data.Text qualified as Text
import Database.PostgreSQL.Migrate
import Database.PostgreSQL.Migrate.CLI.Outcome
import Database.PostgreSQL.Migrate.CLI.Types (checksumText)
import Database.PostgreSQL.Migrate.Internal
import PgMigrate.CLI.Prelude

-- | Render a stable human-oriented outcome.
renderMigrationCommandText :: CliOutcome -> Text
renderMigrationCommandText CliOutcome {command, payload = Left cliError} =
  command <> ": error: " <> renderCliError cliError
renderMigrationCommandText CliOutcome {payload = Right cliPayload} = renderPayload cliPayload

renderCliError :: CliError -> Text
renderCliError cliError =
  case cliError of
    CliInputError err -> err
    CliMigrationError err -> Text.pack (show err)
    CliRepairDefinitionError err -> Text.pack (show err)
    CliRepairError err -> Text.pack (show err)
    CliManifestError err -> Text.pack (show err)
    CliAuthoringError err -> Text.pack (show err)

renderPayload :: CliPayload -> Text
renderPayload cliPayload =
  case cliPayload of
    PlanPayload components -> renderPlan components
    ListPayload migrations -> Text.unlines (renderMigration <$> migrations)
    CheckPayload checked -> Text.unlines (renderChecked <$> NonEmpty.toList checked)
    StatusPayload report -> renderStatus report
    VerifyPayload report -> renderVerification report
    UpPayload report -> renderMigrationReport report
    RepairPayload report -> renderRepairReport report
    NewPayload path -> "created " <> Text.pack path <> "\n"

renderPlan :: [ComponentDescription] -> Text
renderPlan components = Text.unlines (renderComponent <$> components)

renderComponent :: ComponentDescription -> Text
renderComponent (ComponentDescription name position dependencies migrations) =
  Text.intercalate
    " "
    [ Text.pack (show position) <> ".",
      componentNameText name,
      "depends=[" <> Text.intercalate "," (componentNameText <$> Set.toAscList dependencies) <> "]",
      "migrations=" <> Text.pack (show (NonEmpty.length migrations))
    ]

renderMigration :: MigrationDescription -> Text
renderMigration (MigrationDescription identifier position checksum kind mode) =
  Text.intercalate
    " "
    [ renderMigrationId identifier,
      "position=" <> Text.pack (show position),
      "checksum=" <> checksumText checksum,
      "kind=" <> renderKind kind,
      "transaction=" <> renderMode mode
    ]

renderChecked :: CheckedMigration -> Text
renderChecked (CheckedMigration file checksum) =
  Text.pack file <> " checksum=" <> checksumText checksum

renderStatus :: StatusReport -> Text
renderStatus (StatusReport issues applied pending unknown) =
  Text.unlines
    ( [ "applied=" <> Text.pack (show (length applied)),
        "pending=" <> Text.pack (show (length pending)),
        "unknown=" <> Text.pack (show (length unknown)),
        "issues=" <> Text.pack (show (length issues))
      ]
        <> (("applied " <>) . renderMigrationId <$> applied)
        <> (("pending " <>) . renderMigrationId <$> pending)
        <> (("unknown " <>) . renderStoredMigrationId <$> unknown)
        <> (("issue " <>) . Text.pack . show <$> issues)
    )

renderVerification :: VerificationReport -> Text
renderVerification (VerificationReport issues applied pending unknown) =
  Text.unlines
    ( [ if null issues then "verification ok" else "verification failed",
        "applied=" <> Text.pack (show (length applied)),
        "pending=" <> Text.pack (show (length pending)),
        "unknown=" <> Text.pack (show (length unknown))
      ]
        <> (("issue " <>) . Text.pack . show <$> issues)
    )

renderMigrationReport :: MigrationReport -> Text
renderMigrationReport MigrationReport {startedAt, finishedAt, results, cleanupIssues} =
  Text.unlines
    ( [ "started=" <> Text.pack (show startedAt),
        "finished=" <> Text.pack (show finishedAt)
      ]
        <> (renderMigrationResult <$> NonEmpty.toList results)
        <> (renderCleanupIssue <$> cleanupIssues)
    )

renderMigrationResult :: MigrationResult -> Text
renderMigrationResult (MigrationResult identifier outcome duration) =
  Text.intercalate
    " "
    [ renderMigrationId identifier,
      "outcome=" <> renderOutcome outcome,
      "duration_ms=" <> maybe "null" renderDurationMilliseconds duration
    ]

renderRepairReport :: RepairReport -> Text
renderRepairReport RepairReport {repairedMigration, operation, oldStatus, newStatus, cleanupIssues} =
  Text.unlines
    ( Text.intercalate
        " "
        [ renderMigrationId repairedMigration,
          "operation=" <> renderRepairOperation operation,
          "old_status=" <> renderStatusName oldStatus,
          "new_status=" <> renderStatusName newStatus
        ]
        : (renderCleanupIssue <$> cleanupIssues)
    )

renderCleanupIssue :: CleanupIssue -> Text
renderCleanupIssue cleanupIssue = "cleanup_issue=" <> Text.pack (show cleanupIssue)

renderStoredMigrationId :: StoredMigration -> Text
renderStoredMigrationId StoredMigration {storedMigrationId} = renderMigrationId storedMigrationId

renderMigrationId :: MigrationId -> Text
renderMigrationId identifier =
  componentNameText (migrationIdComponent identifier)
    <> "/"
    <> migrationNameText (migrationIdName identifier)

renderKind :: MigrationKind -> Text
renderKind kind = case kind of SqlKind -> "sql"; HaskellKind -> "haskell"

renderMode :: TransactionMode -> Text
renderMode mode = case mode of Transactional -> "transactional"; NonTransactional -> "nontransactional"

renderOutcome :: MigrationOutcome -> Text
renderOutcome outcome = case outcome of AlreadyApplied -> "already_applied"; AppliedNow -> "applied_now"

renderRepairOperation :: RepairOperation -> Text
renderRepairOperation operation = case operation of MarkApplied -> "mark_applied"; Retry -> "retry"

renderStatusName :: MigrationStatus -> Text
renderStatusName status = case status of Running -> "running"; Applied -> "applied"; Failed -> "failed"

renderDurationMilliseconds :: NominalDiffTime -> Text
renderDurationMilliseconds duration = Text.pack (show (round (duration * 1000) :: Integer))