pg-transact-0.1.0.1: src/Database/PostgreSQL/Transact.hs
{-# LANGUAGE FlexibleContexts, GeneralizedNewtypeDeriving, RecordWildCards, OverloadedStrings #-}
module Database.PostgreSQL.Transact where
import Control.Monad.Trans.Reader
import Database.PostgreSQL.Simple as Simple
import Database.PostgreSQL.Simple.Transaction
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Control
import Control.Monad.Catch
import Data.Int
import Control.Monad
import qualified Data.ByteString as BS
newtype DBT m a = DBT { unDBT :: ReaderT Connection m a }
deriving (MonadTrans, MonadThrow)
type DB = DBT IO
instance Functor m => Functor (DBT m) where
fmap f = DBT . fmap f . unDBT
instance Applicative m => Applicative (DBT m) where
pure = DBT . pure
f <*> v = DBT $ unDBT f <*> unDBT v
instance MonadIO m => MonadIO (DBT m) where
liftIO = lift . liftIO
instance Monad m => Monad (DBT m) where
return = lift . return
DBT m >>= k = DBT $ m >>= unDBT . k
isClass25 :: SqlError -> Bool
isClass25 SqlError{..} = BS.take 2 sqlState == "25"
instance (MonadIO m, MonadMask m) => MonadCatch (DBT m) where
catch (DBT act) handler = DBT $ mask $ \restore -> do
conn <- ask
sp <- liftIO $ newSavepoint conn
let setup = catch (restore act) $ \e -> do
liftIO $ rollbackToSavepoint conn sp
unDBT $ handler e
setup `finally` liftIO (tryJust (guard . isClass25) (releaseSavepoint conn sp))
getConnection :: Monad m => DBT m Connection
getConnection = DBT ask
runDBT :: MonadBaseControl IO m => DBT m a -> IsolationLevel -> Connection -> m a
runDBT action level conn
= control
$ \run -> withTransactionLevel level conn
$ run
$ runReaderT (unDBT action) conn
runDBTSerializable :: MonadBaseControl IO m => DBT m a -> Connection -> m a
runDBTSerializable action conn
= control
$ \run -> withTransactionSerializable conn
$ run
$ runReaderT (unDBT action) conn
-- | Perform a @SELECT@ or other SQL query that is expected to return
-- results. All results are retrieved and converted before this
-- function returns.
--
-- When processing large results, this function will consume a lot of
-- client-side memory. Consider using 'fold' instead.
--
-- Exceptions that may be thrown:
--
-- * 'FormatError': the query string could not be formatted correctly.
--
-- * 'QueryError': the result contains no columns (i.e. you should be
-- using 'execute' instead of 'query').
--
-- * 'ResultError': result conversion failed.
--
-- * 'SqlError': the postgresql backend returned an error, e.g.
-- a syntax or type error, or an incorrect table or column name.
query :: (ToRow a, FromRow b, MonadIO m) => Query -> a -> DBT m [b]
query q x = getConnection >>= \conn -> liftIO $ Simple.query conn q x
-- | A version of 'query' that does not perform query substitution.
query_ :: (FromRow b, MonadIO m) => Query -> DBT m [b]
query_ q = getConnection >>= \conn -> liftIO $ Simple.query_ conn q
-- | Execute an @INSERT@, @UPDATE@, or other SQL query that is not
-- expected to return results.
--
-- Returns the number of rows affected.
--
-- Throws 'FormatError' if the query could not be formatted correctly, or
-- a 'SqlError' exception if the backend returns an error.
execute :: (ToRow q, MonadIO m) => Query -> q -> DBT m Int64
execute q x = getConnection >>= \conn -> liftIO $ Simple.execute conn q x
-- | A version of execute that does not perform query substitution.
execute_ :: MonadIO m => Query -> DBT m Int64
execute_ q = getConnection >>= \conn -> liftIO $ Simple.execute_ conn q
-- | Execute a multi-row @INSERT@, @UPDATE@, or other SQL query that is not
-- expected to return results.
--
-- Returns the number of rows affected. If the list of parameters is empty,
-- this function will simply return 0 without issuing the query to the backend.
-- If this is not desired, consider using the 'Values' constructor instead.
--
-- Throws 'FormatError' if the query could not be formatted correctly, or
-- a 'SqlError' exception if the backend returns an error.
--
-- For example, here's a command that inserts two rows into a table
-- with two columns:
--
-- @
-- executeMany [sql|
-- INSERT INTO sometable VALUES (?,?)
-- |] [(1, \"hello\"),(2, \"world\")]
-- @
--
-- Here's an canonical example of a multi-row update command:
--
-- @
-- executeMany [sql|
-- UPDATE sometable
-- SET sometable.y = upd.y
-- FROM (VALUES (?,?)) as upd(x,y)
-- WHERE sometable.x = upd.x
-- |] [(1, \"hello\"),(2, \"world\")]
-- @
executeMany :: (ToRow q, MonadIO m) => Query -> [q] -> DBT m Int64
executeMany q xs = getConnection >>= \conn -> liftIO $ Simple.executeMany conn q xs
-- | Execute @INSERT ... RETURNING@, @UPDATE ... RETURNING@, or other SQL
-- query that accepts multi-row input and is expected to return results.
-- Note that it is possible to write
-- @'query' conn "INSERT ... RETURNING ..." ...@
-- in cases where you are only inserting a single row, and do not need
-- functionality analogous to 'executeMany'.
--
-- If the list of parameters is empty, this function will simply return @[]@
-- without issuing the query to the backend. If this is not desired,
-- consider using the 'Values' constructor instead.
--
-- Throws 'FormatError' if the query could not be formatted correctly.
returning :: (ToRow q, FromRow r, MonadIO m) => Query -> [q] -> DBT m [r]
returning q xs = getConnection >>= \conn -> liftIO $ Simple.returning conn q xs