packages feed

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

module Hasql.Engine.Errors where

import Hasql.Comms.Recv qualified
import Hasql.Comms.ResultDecoder qualified
import Hasql.Comms.Roundtrip qualified
import Hasql.Comms.RowReader qualified
import Hasql.Platform.Prelude
import TextBuilder qualified

-- * Error types

-- |
-- Error that occurs when attempting to establish a database connection.
--
-- These errors can occur when calling 'Hasql.Connection.acquire'.
-- Connection errors are categorized into several types to help with
-- error handling and logging.
data ConnectionError
  = -- | Network-level error while connecting to the database server.
    --
    -- This typically indicates issues like:
    --
    -- * Server is not reachable (wrong host\/port or server is down)
    -- * Network connectivity problems
    -- * Firewall blocking the connection
    -- * Connection timeout
    --
    -- These errors are transient and the operation can be retried.
    NetworkingConnectionError
      -- | Human readable details intended for logging.
      Text
  | -- | Authentication failed when connecting to the database.
    --
    -- This typically indicates issues like:
    --
    -- * Invalid username or password
    -- * User does not have permission to access the database
    -- * Authentication method mismatch (e.g., server requires SSL but client doesn't use it)
    --
    -- These errors are not transient and require fixing the credentials or permissions.
    AuthenticationConnectionError
      -- | Human readable details intended for logging.
      Text
  | -- | Compatibility issue between client and server.
    --
    -- This typically indicates issues like:
    --
    -- * Server version is too old or too new
    -- * Required PostgreSQL features are not available
    -- * Protocol version mismatch
    --
    -- These errors are not transient and require upgrading/downgrading
    -- the server or client.
    CompatibilityConnectionError
      -- | Human readable details intended for logging.
      Text
  | -- | Uncategorized error coming from @libpq@.
    --
    -- This is a catch-all for connection errors that don't fit into other categories.
    -- The error message may be empty if @libpq@ doesn't provide details.
    --
    -- These errors are not transient by default.
    OtherConnectionError
      -- | Human readable details intended for logging. May be empty.
      Text
  deriving stock (Show, Eq)

-- |
-- Error that occurs during session execution.
--
-- A session is a batch of actions executed in a database connection context.
-- Session errors can occur due to statement failures, connection issues,
-- missing types, or internal driver errors.
--
-- Session errors provide detailed context to help diagnose problems,
-- including SQL text, parameters, and the location of the error within
-- a pipeline of statements.
data SessionError
  = -- | An error occurred while executing a statement in the session.
    --
    -- This wraps statement-level errors and provides additional context about:
    --
    -- * Which statement in the pipeline failed (when multiple statements are batched)
    -- * The SQL text and parameters of the failing statement
    -- * Whether the statement was prepared or unprepared
    --
    -- The error message includes formatted output showing all this context,
    -- making it easier to diagnose issues in production.
    StatementSessionError
      -- | Total number of statements in the running pipeline. 1 if it's executed alone.
      Int
      -- | 0-based index of the statement that failed. 0 if it's executed alone.
      Int
      -- | SQL template of the failing statement.
      Text
      -- | Parameter values as text (for logging purposes).
      [Text]
      -- | Whether the statement was executed as a prepared one.
      Bool
      -- | The underlying statement error.
      StatementError
  | -- | An error occurred while executing a script.
    --
    -- Scripts are multi-statement SQL texts executed via 'Hasql.Session.script'.
    -- Unlike regular statements, scripts don't support parameters or result decoding,
    -- and errors are limited to server-reported issues.
    ScriptSessionError
      -- | The SQL text of the script.
      Text
      -- | The server error that occurred.
      ServerError
  | -- | A connection-level error occurred during session execution.
    --
    -- This indicates that the connection to the database was lost or
    -- became unusable during the session. These errors are transient
    -- and the operation can be retried with a new connection.
    --
    -- Note: As of version 1.10, connections recover from async exceptions
    -- without resetting, preserving connection-local state.
    ConnectionSessionError
      -- | Human-readable details about the connection error.
      Text
  | -- | One or more types referenced in the statement could not be found in the database.
    --
    -- This occurs when using custom types (enums, composite types, domains) that
    -- are resolved by name at runtime, but the types don't exist in the database.
    --
    -- To fix this error:
    --
    -- * Ensure the types are defined in the database
    -- * Check that schema search paths are configured correctly
    -- * Verify that the type names in your code match those in the database
    MissingTypesSessionError
      -- | Set of (schema name, type name) pairs that could not be found.
      --
      -- Schema name is 'Nothing' when the type was looked up without a schema qualifier.
      (HashSet (Maybe Text, Text))
  | -- | An internal driver error occurred.
    --
    -- This indicates either:
    --
    -- * A bug in Hasql
    -- * The PostgreSQL server misbehaving
    -- * An unexpected response from the server
    --
    -- If you encounter this error, please report it as a bug.
    DriverSessionError
      -- | Human-readable details about what went wrong.
      Text
  deriving stock (Show, Eq)

-- |
-- Error that occurs when executing a single statement.
--
-- Statement errors can be caused by server-side issues (SQL errors, constraint violations)
-- or by mismatches between the decoder specification and the actual result structure
-- (wrong number of rows/columns, type mismatches, or cell-level decoding failures).
data StatementError
  = -- | The server rejected the statement and returned an error.
    --
    -- This includes SQL syntax errors, constraint violations, permission errors,
    -- and any other error reported by PostgreSQL during statement execution.
    ServerStatementError ServerError
  | -- | The statement returned a different number of rows than expected.
    --
    -- This occurs when using result decoders like @Decoders.singleRow@ or
    -- @Decoders.rowsAffectedAtLeast@ that have specific row count expectations.
    UnexpectedRowCountStatementError
      -- | Expected minimum number of rows.
      Int
      -- | Expected maximum number of rows.
      Int
      -- | Actual number of rows returned.
      Int
  | -- | The statement returned a different number of columns than expected.
    --
    -- This indicates a mismatch between the decoder specification and the
    -- actual result structure, possibly due to:
    --
    -- * Schema changes (columns added or removed)
    -- * Wrong query (selecting different columns than expected)
    -- * Decoder configuration error
    UnexpectedColumnCountStatementError
      -- | Expected number of columns.
      Int
      -- | Actual number of columns returned.
      Int
  | -- | A column has a different type than expected.
    --
    -- This occurs when the decoder expects a specific PostgreSQL type (by OID)
    -- but the actual column has a different type. This can happen due to:
    --
    -- * Schema changes (column type changed)
    -- * Wrong query (selecting wrong column or using a cast)
    -- * Decoder configuration error
    --
    -- Note: As of version 1.10, Hasql performs strict type checking and will
    -- report this error instead of attempting automatic type coercion.
    UnexpectedColumnTypeStatementError
      -- | 0-based column index where the type mismatch occurred.
      Int
      -- | Expected PostgreSQL type OID.
      Word32
      -- | Actual PostgreSQL type OID of the column.
      Word32
  | -- | An error occurred while decoding a specific row.
    --
    -- This wraps errors that occur at the row or cell level, providing
    -- context about which row failed.
    RowStatementError
      -- | 0-based index of the row that failed to decode.
      Int
      -- | The underlying row-level error.
      RowError
  | -- | The database returned an unexpected result structure.
    --
    -- This is a catch-all error that indicates either:
    --
    -- * An improper statement (e.g., executing a query that doesn't match expectations)
    -- * A schema mismatch between the code and database
    -- * A bug in Hasql or server misbehavior
    UnexpectedResultStatementError
      -- | Human-readable details about what went wrong.
      Text
  deriving stock (Show, Eq)

-- |
-- Error reported by the PostgreSQL server when executing a statement.
--
-- The server provides structured error information including error codes
-- (SQL state), messages, and optional context like hints and position information.
--
-- For a complete list of PostgreSQL error codes, see:
-- <https://www.postgresql.org/docs/current/errcodes-appendix.html>
data ServerError
  = ServerError
      -- | SQL State Code (SQLSTATE).
      --
      -- A five-character code that identifies the error class and condition.
      -- Examples: @\"23505\"@ (unique violation), @\"42P01\"@ (undefined table).
      Text
      -- | Primary error message.
      --
      -- A human-readable description of the error.
      Text
      -- | Optional detailed error information.
      --
      -- Additional context about the error, if available.
      (Maybe Text)
      -- | Optional hint for resolving the error.
      --
      -- A suggestion for how to fix the problem, if available.
      (Maybe Text)
      -- | Optional position in the SQL string where the error occurred.
      --
      -- A 1-based character index into the SQL string, if the error
      -- relates to a specific location in the query.
      (Maybe Int)
  deriving stock (Show, Eq)

-- |
-- Error that occurs when decoding a result row.
--
-- Row errors indicate problems when processing an individual row from the result set,
-- either at the cell level or during row refinement/validation.
data RowError
  = -- | An error occurred while decoding a specific cell in the row.
    --
    -- This wraps cell-level errors (null handling, deserialization failures)
    -- and provides context about which column failed and its type.
    CellRowError
      -- | 0-based index of the column where the error occurred.
      Int
      -- | PostgreSQL type OID of the column, as reported by the server.
      Word32
      -- | The underlying cell-level error.
      CellError
  | -- | A refinement or validation error when processing the row.
    --
    -- This occurs when using refinement functions in row decoders
    -- (e.g., with 'Decoders.refineRow') to validate or transform
    -- decoded values. The refinement function rejected the row data.
    RefinementRowError
      -- | Human-readable details about why the refinement failed.
      Text
  deriving stock (Show, Eq)

-- |
-- Error that occurs when decoding a single cell (column value) in a result row.
--
-- Cell errors indicate problems with individual values returned by the database,
-- such as unexpected nulls or failures in binary deserialization.
data CellError
  = -- | A NULL value was encountered when a non-NULL value was expected.
    --
    -- This occurs when using non-nullable decoders (e.g., @Decoders.nonNullable@)
    -- on a column that contains a NULL value.
    UnexpectedNullCellError
  | -- | Failed to deserialize the cell value from its binary representation.
    --
    -- This can occur when:
    --
    -- * The binary data is corrupted
    -- * The decoder doesn't match the actual data type
    -- * The data format is invalid for the expected type
    DeserializationCellError
      -- | Human-readable error message describing what went wrong.
      Text
  deriving stock (Show, Eq)

fromRoundtripError :: Hasql.Comms.Roundtrip.Error context -> SessionError
fromRoundtripError = \case
  Hasql.Comms.Roundtrip.ClientError _context details ->
    ConnectionSessionError (maybe "" decodeUtf8Lenient details)
  Hasql.Comms.Roundtrip.ServerError recvError ->
    fromRecvError (Nothing <$ recvError)

fromRecvError :: Hasql.Comms.Recv.Error (Maybe (Int, Int, ByteString, [Text], Bool)) -> SessionError
fromRecvError = \case
  Hasql.Comms.Recv.ResultError location _resultOffset resultError ->
    case location of
      Nothing ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Unexpected error outside of statement context. ",
            "This indicates a bug in Hasql or the server misbehaving. ",
            "Error: ",
            TextBuilder.string (show resultError)
          ]
      Just (totalStatements, statementIndex, sql, parameters, prepared) ->
        StatementSessionError
          totalStatements
          statementIndex
          (decodeUtf8Lenient sql)
          parameters
          prepared
          (fromStatementResultError resultError)
  Hasql.Comms.Recv.NoResultsError location details ->
    case location of
      Nothing ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Unexpectedly got no results outside of statement context. ",
            "This indicates a bug in Hasql or the server misbehaving. ",
            "Details: ",
            TextBuilder.string (show details)
          ]
      Just (totalStatements, statementIndex, sql, parameters, prepared) ->
        StatementSessionError
          totalStatements
          statementIndex
          (decodeUtf8Lenient sql)
          parameters
          prepared
          (UnexpectedRowCountStatementError 1 1 0)
  Hasql.Comms.Recv.TooManyResultsError location actual ->
    case location of
      Nothing ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Unexpectedly got too many results outside of statement context. ",
            "This indicates a bug in Hasql or the server misbehaving. ",
            "Amount: ",
            TextBuilder.decimal actual
          ]
      Just (totalStatements, statementIndex, sql, parameters, prepared) ->
        StatementSessionError
          totalStatements
          statementIndex
          (decodeUtf8Lenient sql)
          parameters
          prepared
          (UnexpectedRowCountStatementError 1 1 actual)

fromStatementResultError :: Hasql.Comms.ResultDecoder.Error -> StatementError
fromStatementResultError = \case
  Hasql.Comms.ResultDecoder.ServerError code message detail hint position ->
    ServerStatementError
      ( ServerError
          (decodeUtf8Lenient code)
          (decodeUtf8Lenient message)
          (fmap decodeUtf8Lenient detail)
          (fmap decodeUtf8Lenient hint)
          position
      )
  Hasql.Comms.ResultDecoder.UnexpectedResult msg ->
    UnexpectedResultStatementError msg
  Hasql.Comms.ResultDecoder.UnexpectedRowCount actual ->
    UnexpectedRowCountStatementError 1 1 actual
  Hasql.Comms.ResultDecoder.UnexpectedColumnCount expected actual ->
    UnexpectedColumnCountStatementError expected actual
  Hasql.Comms.ResultDecoder.DecoderTypeMismatch colIdx expectedOid actualOid ->
    UnexpectedColumnTypeStatementError
      colIdx
      expectedOid
      actualOid
  Hasql.Comms.ResultDecoder.RowError rowIndex rowError ->
    case rowError of
      Hasql.Comms.RowReader.CellError column oid cellErr ->
        RowStatementError
          rowIndex
          ( CellRowError
              column
              oid
              ( case cellErr of
                  Hasql.Comms.RowReader.DecodingCellError msg -> DeserializationCellError msg
                  Hasql.Comms.RowReader.UnexpectedNullCellError -> UnexpectedNullCellError
              )
          )
      Hasql.Comms.RowReader.RefinementError msg ->
        RowStatementError
          rowIndex
          (RefinementRowError msg)

fromRecvErrorInScript :: ByteString -> Hasql.Comms.Recv.Error (Maybe ByteString) -> SessionError
fromRecvErrorInScript scriptSql = \case
  Hasql.Comms.Recv.ResultError _ _ resultError ->
    case resultError of
      Hasql.Comms.ResultDecoder.ServerError code message detail hint position ->
        ScriptSessionError
          (decodeUtf8Lenient scriptSql)
          ( ServerError
              (decodeUtf8Lenient code)
              (decodeUtf8Lenient message)
              (fmap decodeUtf8Lenient detail)
              (fmap decodeUtf8Lenient hint)
              position
          )
      Hasql.Comms.ResultDecoder.UnexpectedResult msg ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Unexpected result in script: ",
            TextBuilder.text msg
          ]
      Hasql.Comms.ResultDecoder.UnexpectedRowCount actual ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Unexpected amount of rows in script: ",
            TextBuilder.decimal actual
          ]
      Hasql.Comms.ResultDecoder.UnexpectedColumnCount expected actual ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Unexpected amount of columns in script: expected ",
            TextBuilder.decimal expected,
            ", got ",
            TextBuilder.decimal actual
          ]
      Hasql.Comms.ResultDecoder.DecoderTypeMismatch colIdx expectedOid actualOid ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Decoder type mismatch in script: expected OID ",
            TextBuilder.string (show expectedOid),
            " at column ",
            TextBuilder.decimal colIdx,
            ", got ",
            TextBuilder.string (show actualOid)
          ]
      Hasql.Comms.ResultDecoder.RowError rowIndex rowError ->
        (DriverSessionError . TextBuilder.toText . mconcat)
          [ "Row error in script at row ",
            TextBuilder.decimal rowIndex,
            ": ",
            TextBuilder.string (show rowError)
          ]
  Hasql.Comms.Recv.NoResultsError _ details ->
    (DriverSessionError . TextBuilder.toText . mconcat)
      [ "Got no results in script.",
        " This indicates a bug in Hasql or the server misbehaving.",
        details
          & filter (/= "")
          & foldMap (mappend " Details: " . TextBuilder.text . decodeUtf8Lenient)
      ]
  Hasql.Comms.Recv.TooManyResultsError _ actual ->
    (DriverSessionError . TextBuilder.toText . mconcat)
      [ "Got too many results in script. ",
        "This indicates a bug in Hasql or the server misbehaving. ",
        "Amount: ",
        TextBuilder.decimal actual
      ]