packages feed

pg-wire-0.2.0.0: src/PgWire/Error.hs

-- | Error types for pg-wire runtime exceptions.
--
-- All errors are thrown as 'PgWireError' via 'throwIO' and can be caught
-- with the standard @Control.Exception@ machinery.
module PgWire.Error
  ( PgWireError (..)
  , throwPgWire
  , isFatal
  ) where

import Control.Exception (Exception, throwIO)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import GHC.Generics (Generic)
import NoThunks.Class (NoThunks)
import PgWire.Protocol.Backend (PgError (..))

-- | All runtime errors thrown by pg-wire.
data PgWireError
  = ConnectionError ByteString
  -- ^ Failed to establish a TCP\/TLS connection to the server.
  | AuthError ByteString
  -- ^ Authentication failed (wrong password, unsupported mechanism, etc.).
  | ProtocolError ByteString
  -- ^ Unexpected message in the wire protocol (indicates a driver bug or
  -- incompatible server).
  | QueryError PgError
  -- ^ The server returned an error in response to a query. The 'PgError'
  -- contains SQLSTATE, message, detail, hint, and position fields.
  | DecodeError ByteString
  -- ^ Failed to decode a result row (type mismatch, unexpected NULL, or
  -- no rows returned to 'fetchOneOrThrow').
  | PoolTimeout
  -- ^ Timed out waiting to acquire a connection from the pool.
  | PoolClosed
  -- ^ Attempted to use a pool after 'PgWire.Pool.closePool' was called.
  | ConnectionDead
  -- ^ The connection's reader thread has died (network failure, server crash).
  deriving stock (Show, Eq, Generic)

instance NoThunks PgWireError
instance Exception PgWireError

-- | Throw a 'PgWireError' as an exception.
throwPgWire :: PgWireError -> IO a
throwPgWire = throwIO

-- | Classify whether an error indicates the connection is unusable.
--
-- Fatal errors mean the connection should be destroyed rather than returned
-- to the pool. Recoverable errors (constraint violations, syntax errors,
-- serialization failures) leave the connection in a usable state.
--
-- Fatal error classes:
--
-- * 'ConnectionError', 'AuthError', 'ProtocolError', 'ConnectionDead' —
--   always fatal.
-- * SQLSTATE class @08@ — connection exception (server closed it).
-- * SQLSTATE class @57@ — operator intervention (admin shutdown, crash recovery).
-- * SQLSTATE class @58@ — system error (I\/O error, out of memory on server).
-- * SQLSTATE @F0000@ — configuration file error.
-- * SQLSTATE @XX000@ — internal error (server bug).
--
-- Everything else (constraint violations, query errors, transaction errors,
-- syntax errors, etc.) is recoverable.
isFatal :: PgWireError -> Bool
isFatal ConnectionError{} = True
isFatal AuthError{} = True
isFatal ProtocolError{} = True
isFatal ConnectionDead{} = True
isFatal PoolTimeout{} = False
isFatal PoolClosed{} = False
isFatal DecodeError{} = False
isFatal (QueryError err) = isFatalState (pgCode err)

-- | Check if a SQLSTATE code indicates a fatal error.
isFatalState :: ByteString -> Bool
isFatalState code
  | BS.length code >= 2 =
      let cls = BS.take 2 code
       in cls == "08"     -- connection exception
       || cls == "57"     -- operator intervention
       || cls == "58"     -- system error
       || code == "F0000" -- configuration file error
       || code == "XX000" -- internal error
  | otherwise = False