{-# OPTIONS_GHC -Wno-orphans #-} -- | Access to a PostgreSQL database via 'MonadDB'. module Effectful.HPQTypes ( -- * Effect DB (..) -- ** Handlers , runDB -- * Re-exports , module Database.PostgreSQL.PQTypes ) where import Database.PostgreSQL.PQTypes import Database.PostgreSQL.PQTypes.Internal.Connection qualified as PQ import Database.PostgreSQL.PQTypes.Internal.Notification qualified as PQ import Database.PostgreSQL.PQTypes.Internal.State qualified as PQ import Effectful import Effectful.Dispatch.Dynamic import Effectful.Exception import Effectful.State.Static.Local qualified as State import GHC.Stack -- | Provide the ability to access a PostgreSQL database via 'MonadDB'. data DB :: Effect where RunQuery :: IsSQL sql => sql -> DB m Int RunPreparedQuery :: IsSQL sql => PQ.QueryName -> sql -> DB m Int GetLastQuery :: DB m (BackendPid, SomeSQL) WithFrozenLastQuery :: m a -> DB m a GetConnectionStats :: DB m PQ.ConnectionStats GetQueryResult :: FromRow row => DB m (Maybe (QueryResult row)) ClearQueryResult :: DB m () GetConnectionAcquisitionMode :: DB m ConnectionAcquisitionMode AcquireAndHoldConnection :: IsolationLevel -> Permissions -> DB m () UnsafeAcquireOnDemandConnection :: DB m () GetNotification :: Int -> DB m (Maybe PQ.Notification) WithNewConnection :: m a -> DB m a type instance DispatchOf DB = Dynamic -- | Orphan, canonical instance. instance DB :> es => MonadDB (Eff es) where runQuery = withFrozenCallStack $ send . RunQuery runPreparedQuery name = withFrozenCallStack $ send . RunPreparedQuery name getLastQuery = send GetLastQuery withFrozenLastQuery = send . WithFrozenLastQuery getConnectionStats = withFrozenCallStack $ send GetConnectionStats getQueryResult = send GetQueryResult clearQueryResult = send ClearQueryResult getConnectionAcquisitionMode = send GetConnectionAcquisitionMode acquireAndHoldConnection isoLevel = send . AcquireAndHoldConnection isoLevel unsafeAcquireOnDemandConnection = send UnsafeAcquireOnDemandConnection getNotification = send . GetNotification withNewConnection = send . WithNewConnection -- | Run the 'DB' effect with the given connection source and transaction -- settings. -- -- /Note:/ this is the @effectful@ version of 'runDBT'. runDB :: IOE :> es => PQ.ConnectionSourceM (Eff es) -- ^ Connection source. -> TransactionSettings -- ^ Transaction settings. -> Eff (DB : es) a -> Eff es a runDB cs0 ts0 m = PQ.withConnectionData cs0 ts0 $ \cd0 -> do reinterpretWith (State.evalState $ PQ.mkDBState cd0 ts0) m $ \env -> \case RunQuery sql -> modifyState $ \st -> withFrozenCallStack $ do PQ.withConnection (PQ.dbConnectionData st) $ \conn -> do liftIO $ PQ.updateStateWith conn st sql =<< PQ.runQueryIO conn sql RunPreparedQuery name sql -> modifyState $ \st -> withFrozenCallStack $ do PQ.withConnection (PQ.dbConnectionData st) $ \conn -> do liftIO $ PQ.updateStateWith conn st sql =<< PQ.runPreparedQueryIO conn name sql GetLastQuery -> PQ.dbLastQuery <$> get WithFrozenLastQuery action -> do origValue <- PQ.dbRecordLastQuery <$> get bracket_ (modify $ \st -> st {PQ.dbRecordLastQuery = False}) (modify $ \st -> st {PQ.dbRecordLastQuery = origValue}) (localSeqUnlift env $ \unlift -> unlift action) GetConnectionStats -> PQ.dbConnectionStats <$> get GetQueryResult -> PQ.dbQueryResult <$> get ClearQueryResult -> modify $ \st -> st {PQ.dbQueryResult = Nothing} GetConnectionAcquisitionMode -> do liftIO . PQ.getConnectionAcquisitionModeIO . PQ.dbConnectionData =<< get AcquireAndHoldConnection isolationLevel permissions -> withState $ \st -> do PQ.changeAcquisitionModeTo (AcquireAndHold isolationLevel permissions) (PQ.dbConnectionData st) UnsafeAcquireOnDemandConnection -> withState $ \st -> do PQ.changeAcquisitionModeTo AcquireOnDemand (PQ.dbConnectionData st) GetNotification time -> withState $ \st -> do PQ.withConnection (PQ.dbConnectionData st) $ \conn -> do liftIO $ PQ.getNotificationIO conn time WithNewConnection action -> do st <- get cam <- liftIO . PQ.getConnectionAcquisitionModeIO $ PQ.dbConnectionData st let cs = PQ.getConnectionSource $ PQ.dbConnectionData st ts = TransactionSettings { tsRestartPredicate = PQ.dbRestartPredicate st , tsConnectionAcquisitionMode = cam } raiseWith SeqUnlift $ \lower -> do PQ.withConnectionData cs ts $ \cd -> lower $ do localSeqUnlift env $ \unlift -> do bracket_ (put $ PQ.mkDBState cd ts) (put st) $ unlift action ---------------------------------------- -- Internal helpers -- | Internal state used to reinterpret the `DB` effect type DBState es = PQ.DBState (Eff es) get :: Eff (State.State (DBState es) : es) (DBState es) get = State.get put :: DBState es -> Eff (State.State (DBState es) : es) () put = State.put modify :: (DBState es -> DBState es) -> Eff (State.State (DBState es) : es) () modify = State.modify modifyState :: (DBState es -> Eff es (a, DBState es)) -> Eff (State.State (DBState es) : es) a modifyState action = do s0 <- get (a, s) <- raise $ action s0 put s pure a withState :: (DBState es -> Eff es a) -> Eff (State.State (DBState es) : es) a withState action = raise . action =<< get