{-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE FlexibleInstances #-} -- | SQL Effect class, basically capturing some way of accessing a database. module Preql.Effect ( module Preql.Effect, Transaction ) where import Preql.Effect.Internal import Preql.Imports import Preql.Wire import Control.Exception (throwIO) import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Trans.Except (ExceptT(..), runExceptT) import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT) import Control.Monad.Trans.Reader (ReaderT(..), ask, runReaderT) import Database.PostgreSQL.LibPQ (Connection) import qualified Control.Monad.Trans.State.Lazy as SL import qualified Control.Monad.Trans.State.Strict as SS import qualified Control.Monad.Trans.RWS.Lazy as L import qualified Control.Monad.Trans.RWS.Strict as S import qualified Preql.Wire.Query as W -- | An Effect class for running SQL queries. You can think of this as a context -- specifying a particular Postgres connection (or connection pool). A minimal instance -- defines @withConnection@. -- -- Override the remaining methods to log errors before rethrowing, or not to rethrow. class SqlQuery m => SQL (m :: * -> *) where -- | Run multiple queries in a transaction. runTransaction' :: IsolationLevel -> Transaction a -> m a default runTransaction' :: MonadIO m => IsolationLevel -> Transaction a -> m a runTransaction' level t = withConnection $ \conn -> liftIO $ either throwIO pure =<< runTransactionIO level t conn -- | @runTransaction@ covers the most common patterns of -- mult-statement transactions. @withConnection@ is useful when -- you want more control, or want to override the defaults that -- your instance defines. For example: -- - change the number of retries -- - interleave calls to other services with the Postgres transaction -- - ensure a prepared statement is shared among successive transactions withConnection :: (Connection -> m a) -> m a -- | Run a query on the specified 'Connection' queryOn :: (ToSql p, FromSql r) => Connection -> (Query, p) -> m (Vector r) default queryOn :: (ToSql p, FromSql r, MonadIO m) => Connection -> (Query, p) -> m (Vector r) queryOn conn (q, p) = liftIO $ either throwIO pure =<< W.query conn q p queryOn_ :: ToSql p => Connection -> (Query, p) -> m () default queryOn_ :: (ToSql p, MonadIO m) => Connection -> (Query, p) -> m () queryOn_ conn (q, p) = liftIO $ either throwIO pure =<< W.query_ conn q p -- | Run a Transaction with full Serializable isolation. runTransaction :: SQL m => Transaction a -> m a runTransaction = runTransaction' Serializable -- | SqlQuery is separate from 'SQL' so that nested 'Transaction's are -- statically prevented. @query@ can be used directly within any -- 'SQL' monad (running a single-statement transaction), or within a -- 'Transaction'. -- -- Users should not need to define instances, as every @SQL@ instance -- implies a @SqlQuery@ instance. class Monad m => SqlQuery (m :: * -> *) where -- | Run a parameterized query that returns data. The tuple argument is typically provided by -- the 'sql' Quasiquoter. query :: (ToSql p, FromSql r) => (Query, p) -> m (Vector r) -- | Run a parameterized query that does not return data. query_ :: ToSql p => (Query, p) -> m () -- | Most larger applications will define an instance; this one is suitable to test out the library. instance SQL (ReaderT Connection IO) where withConnection = (ask >>=) instance {-# OVERLAPPABLE #-} (Monad m, SQL m) => SqlQuery m where query qp = withConnection (\conn -> queryOn conn qp) query_ q = withConnection (\conn -> queryOn_ conn q) -- * Transactions -- | Run the provided 'Transaction'. If it fails with a 'QueryError', roll back. runTransactionIO :: IsolationLevel -> Transaction a -> Connection -> IO (Either QueryError a) runTransactionIO level (Transaction m) conn = do either throwIO pure =<< W.begin conn level e_a <- runReaderT (runExceptT m) conn void $ case e_a of Left _ -> W.rollback conn Right _ -> W.commit conn return e_a -- | The same @query@ methods can be used within a @Transaction@. -- Nested @Transactions@ are implemented using savepoints. instance SqlQuery Transaction where query (q, p) = Transaction (ExceptT (ReaderT (\conn -> W.query conn q p))) query_ (q, p) = Transaction (ExceptT (ReaderT (\conn -> W.query_ conn q p))) -- Transformer instances -- These are all the same, except for the @withConnection@ definitions. instance SQL m => SQL (ExceptT e m) where withConnection f = ExceptT (withConnection (runExceptT . f)) runTransaction' level t = lift $ runTransaction' level t queryOn conn qp = lift $ queryOn conn qp queryOn_ conn q = lift $ queryOn_ conn q instance {-# OVERLAPPABLE #-} SQL m => SQL (ReaderT r m) where withConnection f = ReaderT (\r -> withConnection (\conn -> runReaderT (f conn) r)) runTransaction' level t = lift $ runTransaction' level t queryOn conn qp = lift $ queryOn conn qp queryOn_ conn q = lift $ queryOn_ conn q instance SQL m => SQL (MaybeT m) where withConnection f = MaybeT (withConnection (runMaybeT . f)) runTransaction' level t = lift $ runTransaction' level t queryOn conn qp = lift $ queryOn conn qp queryOn_ conn q = lift $ queryOn_ conn q instance SQL m => SQL (SL.StateT s m) where withConnection f = SL.StateT (\s -> withConnection (\conn -> SL.runStateT (f conn) s)) runTransaction' level t = lift $ runTransaction' level t queryOn conn qp = lift $ queryOn conn qp queryOn_ conn q = lift $ queryOn_ conn q instance SQL m => SQL (SS.StateT s m) where withConnection f = SS.StateT (\s -> withConnection (\conn -> SS.runStateT (f conn) s)) runTransaction' level t = lift $ runTransaction' level t queryOn conn qp = lift $ queryOn conn qp queryOn_ conn q = lift $ queryOn_ conn q instance (Monoid w, SQL m) => SQL (S.RWST r w s m) where withConnection f = S.RWST (\r s -> withConnection (\conn -> S.runRWST (f conn) r s)) runTransaction' level t = lift $ runTransaction' level t queryOn conn qp = lift $ queryOn conn qp queryOn_ conn q = lift $ queryOn_ conn q instance (Monoid w, SQL m) => SQL (L.RWST r w s m) where withConnection f = L.RWST (\r s -> withConnection (\conn -> L.runRWST (f conn) r s)) runTransaction' level t = lift $ runTransaction' level t queryOn conn qp = lift $ queryOn conn qp queryOn_ conn q = lift $ queryOn_ conn q