packages feed

postgresql-tx-squeal-0.2.0.0: src/Database/PostgreSQL/Tx/Squeal/Internal.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralisedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
module Database.PostgreSQL.Tx.Squeal.Internal
  ( -- * Disclaimer
    -- $disclaimer

    -- ** Internals
    module Database.PostgreSQL.Tx.Squeal.Internal
  ) where

import Control.Exception (Exception(fromException))
import Control.Monad.IO.Class (MonadIO(liftIO))
import Data.Kind (Constraint)
import Database.PostgreSQL.Tx (TxEnv, TxException, TxM, askTxEnv, mapExceptionTx)
import Database.PostgreSQL.Tx.Squeal.Internal.Reexport
import Database.PostgreSQL.Tx.Unsafe (unsafeLookupTxEnvIO, unsafeMkTxException, unsafeRunIOInTxM, unsafeRunTxM)
import GHC.TypeLits (ErrorMessage(Text), TypeError)
import UnliftIO (MonadUnliftIO)
import qualified Data.ByteString.Char8 as Char8
import qualified Database.PostgreSQL.LibPQ as LibPQ
import qualified Squeal.PostgreSQL as Squeal
import qualified UnliftIO

-- | Runtime environment needed to run @squeal-postgresql@ via @postgresql-tx@.
--
-- @since 0.2.0.0
type SquealEnv r =
  (TxEnv SquealConnection r) :: Constraint

-- | Monad type alias for running @squeal-postgresql@ via @postgresql-tx@.
--
-- @since 0.2.0.0
type SquealM a = forall r. (SquealEnv r) => TxM r a

-- | Alias for 'SquealTxM'' but has the 'SquealEnv' constraint applied to @r@.
--
-- @since 0.2.0.0
type SquealTxM (db :: SchemasType) a =
  forall r. (SquealEnv r) => SquealTxM' db r a

-- | A newtype wrapper around 'TxM' which includes the @squeal@ 'SchemasType'
-- parameter @db@. This is used only as type information.
-- You can easily convert 'TxM' to and from 'SquealTxM'' by using the
-- 'SquealTxM'' constructor and 'fromSquealTxM' function, respectively.
--
-- In practice, you will likely prefer to use the 'SquealTxM' type alias
-- as it includes the 'SquealEnv' constraint on @r@.
--
-- @since 0.2.0.0
newtype SquealTxM' (db :: SchemasType) r a =
  SquealTxM
    { -- | Convert a 'SquealTxM'' to a 'TxM'.
      --
      -- @since 0.2.0.0
      fromSquealTxM :: TxM r a
    }
  deriving newtype (Functor, Applicative, Monad)

-- | The 'SquealTxM'' monad discourages performing arbitrary 'IO' within a
-- transaction, so this instance generates a type error when client code tries
-- to call 'liftIO'.
--
-- Note that we specialize this instance for 'SquealTxM'' rather than derive it
-- via newtype so we can provide a better error message.
--
-- @since 0.2.0.0
instance
  ( TypeError
      ('Text "MonadIO is banned in SquealTxM'; use 'SquealTxM . unsafeRunIOInTxM' if you are sure this is safe IO")
  ) => MonadIO (SquealTxM' db r)
  where
  liftIO = undefined

-- | Used in the 'SquealEnv' to specify the 'LibPQ.Connection' to use.
-- Should produce the same 'LibPQ.Connection' if called multiple times
-- in the same transaction. Usually you will want to use 'mkSquealConnection'
-- to get one.
--
-- @since 0.2.0.0
newtype SquealConnection =
  UnsafeSquealConnection
    { unsafeGetLibPQConnection :: IO LibPQ.Connection
    }

-- | Construct a 'SquealConnection' from a 'LibPQ.Connection'.
--
-- @since 0.2.0.0
mkSquealConnection :: LibPQ.Connection -> SquealConnection
mkSquealConnection conn = UnsafeSquealConnection (pure conn)

fromSquealException :: SquealException -> TxException
fromSquealException =
  unsafeMkTxException \case
    SQLException SQLState { sqlStateCode } -> Just $ Char8.unpack sqlStateCode
    _ -> Nothing

unsafeSquealIOTxM
  :: PQ db db IO a
  -> SquealTxM db a
unsafeSquealIOTxM (PQ f) = SquealTxM $ mapExceptionTx (Just . fromSquealException) do
  UnsafeSquealConnection { unsafeGetLibPQConnection } <- askTxEnv
  unsafeRunIOInTxM do
    conn <- unsafeGetLibPQConnection
    K a <- f (K conn)
    pure a

unsafeSquealIOTxM1
  :: (x1 -> PQ db db IO a)
  -> x1 -> SquealTxM db a
unsafeSquealIOTxM1 f x1 = unsafeSquealIOTxM $ f x1

unsafeSquealIOTxM2
  :: (x1 -> x2 -> PQ db db IO a)
  -> x1 -> x2 -> SquealTxM db a
unsafeSquealIOTxM2 f x1 x2 = unsafeSquealIOTxM $ f x1 x2

unsafeSquealIOTxM3
  :: (x1 -> x2 -> x3 -> PQ db db IO a)
  -> x1 -> x2 -> x3 -> SquealTxM db a
unsafeSquealIOTxM3 f x1 x2 x3 = unsafeSquealIOTxM $ f x1 x2 x3

unsafeRunSquealTransaction
  :: forall r a. (SquealEnv r)
  => (forall db. PQ db db IO a -> PQ db db IO a)
  -> r
  -> TxM r a
  -> IO a
unsafeRunSquealTransaction f r x = do
  UnsafeSquealConnection { unsafeGetLibPQConnection } <- unsafeLookupTxEnvIO r
  conn <- unsafeGetLibPQConnection
  flip evalPQ (K conn)
    $ f
    $ PQ \_ -> K <$> unsafeRunTxM r x

-- | A variant of 'transactionallyRetry' which takes a predicate
-- for determining when to retry instead of only doing so on
-- @serialization_failure@.
transactionallyRetry'
  :: (MonadUnliftIO m, MonadPQ db m, Exception e)
  => TransactionMode
  -> (e -> Bool)
  -> m a
  -> m a
transactionallyRetry' mode shouldRetry action = UnliftIO.mask $ \restore ->
  loop . UnliftIO.try $ do
    x <- restore action
    Squeal.manipulate_ commit
    return x
  where
  loop attempt = do
    Squeal.manipulate_ $ begin mode
    attempt >>= \case
      Right a -> return a
      Left e -> do
        Squeal.manipulate_ rollback
        if any shouldRetry (fromException e) then
          loop attempt
        else
          UnliftIO.throwIO e

-- $disclaimer
--
-- Changes to this module will not be reflected in the library's version
-- updates.