packages feed

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

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

import Data.ByteString qualified as ByteString
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.Internal
import Numeric qualified
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
    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) =
  Text.unlines
    ( [ "started=" <> Text.pack (show startedAt),
        "finished=" <> Text.pack (show finishedAt)
      ]
        <> (renderMigrationResult <$> NonEmpty.toList results)
    )

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 identifier operation oldStatus newStatus) =
  Text.intercalate
    " "
    [ renderMigrationId identifier,
      "operation=" <> renderRepairOperation operation,
      "old_status=" <> renderStatusName oldStatus,
      "new_status=" <> renderStatusName newStatus
    ]
    <> "\n"

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

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

checksumText :: MigrationChecksum -> Text
checksumText =
  Text.pack . concatMap renderByte . ByteString.unpack . migrationChecksumBytes
  where
    renderByte byte =
      case Numeric.showHex byte "" of
        [digit] -> ['0', digit]
        digits -> digits

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))