packages feed

hasql-1.10: src/library/Hasql/Errors.hs

-- |
-- Explicit error types for all Hasql operations.
--
-- This module provides access to all error types used throughout Hasql:
--
-- * 'ConnectionError' - errors that occur when establishing a database connection
-- * 'SessionError' - errors that occur during session execution
--
-- The module follows Hasql's philosophy of explicit error handling,
-- where all errors are represented as values rather than exceptions.
module Hasql.Errors
  ( -- * Error class
    IsError (..),
    toDetailedText,

    -- * Connection errors
    ConnectionError (..),

    -- * Session errors
    SessionError (..),
    StatementError (..),
    RowError (..),
    CellError (..),
    ServerError (..),
  )
where

import Data.HashSet qualified as HashSet
import Data.Text qualified as Text
import Hasql.Engine.Errors
import Hasql.Platform.Prelude
import TextBuilder qualified

-- * Classes

-- | A class for types that can be treated as errors.
class IsError a where
  -- | Convert the error to a human-readable message with no dynamic details.
  toMessage :: a -> Text

  -- | Convert the error to a list of key-value pairs of dynamic details.
  toDetails :: a -> [(Text, Text)]

  -- | Whether the error is transient and the operation causing it can be retried.
  isTransient :: a -> Bool

-- | Convert the error to a multiline detailed human-readable text representation containing all details.
toDetailedText :: (IsError e) => e -> Text
toDetailedText = TextBuilder.toText . toDetailedTextBuilder

-- | Convert the error to a multiline detailed human-readable text representation containing all details.
toDetailedTextBuilder :: (IsError e) => e -> TextBuilder
toDetailedTextBuilder err =
  TextBuilder.text (toMessage err)
    <> foldMap
      ( \(key, value) ->
          mconcat
            [ "\n  ",
              TextBuilder.text key,
              case Text.lines value of
                [] -> ":"
                [singleLine] ->
                  ": " <> TextBuilder.text singleLine
                multipleLines ->
                  ":" <> foldMap (mappend "\n    " . TextBuilder.text) multipleLines
            ]
      )
      (toDetails err)

-- * Instances

instance IsError ConnectionError where
  toMessage = \case
    NetworkingConnectionError {} ->
      "Networking error while connecting to the database"
    AuthenticationConnectionError {} ->
      "Authentication error while connecting to the database"
    CompatibilityConnectionError {} ->
      "Compatibility error while connecting to the database"
    OtherConnectionError {} ->
      "Connection error while connecting to the database"

  toDetails = \case
    NetworkingConnectionError reason ->
      [("reason", reason)]
    AuthenticationConnectionError reason ->
      [("reason", reason)]
    CompatibilityConnectionError reason ->
      [("reason", reason)]
    OtherConnectionError reason ->
      [("reason", reason)]

  isTransient = \case
    NetworkingConnectionError {} -> True
    AuthenticationConnectionError {} -> False
    CompatibilityConnectionError {} -> False
    OtherConnectionError {} -> False

instance IsError ServerError where
  toMessage _ =
    "Server error"

  toDetails (ServerError code message detail hint position) =
    mconcat
      [ [("code", code), ("message", message)],
        maybe [] (\d -> [("detail", d)]) detail,
        maybe [] (\h -> [("hint", h)]) hint,
        maybe [] (\p -> [("position", (TextBuilder.toText . TextBuilder.decimal) p)]) position
      ]

  isTransient = const False

instance IsError CellError where
  toMessage = \case
    UnexpectedNullCellError ->
      "Unexpected null value"
    DeserializationCellError {} ->
      "Failed to deserialize cell"

  toDetails = \case
    UnexpectedNullCellError ->
      []
    DeserializationCellError reason ->
      [("reason", reason)]

  isTransient = const False

instance IsError StatementError where
  toMessage = \case
    ServerStatementError executionError ->
      toMessage executionError
    UnexpectedRowCountStatementError {} ->
      "Unexpected number of rows"
    UnexpectedColumnCountStatementError {} ->
      "Unexpected number of columns"
    UnexpectedColumnTypeStatementError {} ->
      "Unexpected column type"
    RowStatementError _ rowError ->
      toMessage rowError
    UnexpectedResultStatementError {} ->
      "Driver error"

  toDetails = \case
    ServerStatementError executionError ->
      toDetails executionError
    UnexpectedRowCountStatementError min max actual ->
      [ ("expectedMin", (TextBuilder.toText . TextBuilder.decimal) min),
        ("expectedMax", (TextBuilder.toText . TextBuilder.decimal) max),
        ("actual", (TextBuilder.toText . TextBuilder.decimal) actual)
      ]
    UnexpectedColumnCountStatementError expected actual ->
      [ ("expected", (TextBuilder.toText . TextBuilder.decimal) expected),
        ("actual", (TextBuilder.toText . TextBuilder.decimal) actual)
      ]
    UnexpectedColumnTypeStatementError colIdx expected actual ->
      [ ("columnIndex", (TextBuilder.toText . TextBuilder.decimal) colIdx),
        ("expectedOid", (TextBuilder.toText . TextBuilder.decimal) expected),
        ("actualOid", (TextBuilder.toText . TextBuilder.decimal) actual)
      ]
    RowStatementError rowIdx rowError ->
      ("rowIndex", (TextBuilder.toText . TextBuilder.decimal) rowIdx) : toDetails rowError
    UnexpectedResultStatementError reason ->
      [("reason", reason)]

  isTransient = const False

instance IsError RowError where
  toMessage = \case
    CellRowError _ _ cellErr ->
      toMessage cellErr
    RefinementRowError {} ->
      "Refinement error"

  toDetails = \case
    CellRowError colIdx oid cellErr ->
      [ ("columnIndex", (TextBuilder.toText . TextBuilder.decimal) colIdx),
        ("oid", (TextBuilder.toText . TextBuilder.decimal) oid)
      ]
        <> toDetails cellErr
    RefinementRowError reason ->
      [("reason", reason)]

  isTransient = const False

instance IsError SessionError where
  toMessage = \case
    StatementSessionError _ _ _ _ _ statementError ->
      toMessage statementError
    ScriptSessionError _ execErr ->
      toMessage execErr
    ConnectionSessionError {} ->
      "Connection error"
    DriverSessionError {} ->
      "Driver error"
    MissingTypesSessionError {} ->
      "Types not found in database"

  toDetails = \case
    StatementSessionError totalStatements statementIndex sql parameters prepared statementError ->
      [ ("totalStatements", (TextBuilder.toText . TextBuilder.decimal) totalStatements),
        ("statementIndex", (TextBuilder.toText . TextBuilder.decimal) statementIndex),
        ("sql", sql),
        ("parameters", Text.intercalate ", " parameters),
        ("prepared", if prepared then "true" else "false")
      ]
        <> toDetails statementError
    ScriptSessionError sql execErr ->
      ("sql", sql) : toDetails execErr
    ConnectionSessionError reason ->
      [("reason", reason)]
    DriverSessionError reason ->
      [("reason", reason)]
    MissingTypesSessionError missingTypes ->
      [ ( "missingTypes",
          (TextBuilder.toText . mconcat . intersperse ", " . fmap formatType . HashSet.toList) missingTypes
        )
      ]
      where
        formatType (schema, name) = maybe "" ((<> ".") . TextBuilder.text) schema <> TextBuilder.text name

  isTransient = \case
    ConnectionSessionError _ -> True
    StatementSessionError {} -> False
    ScriptSessionError {} -> False
    DriverSessionError {} -> False
    MissingTypesSessionError {} -> False