@@ -1,10 +1,11 @@
1{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2module ProjectM36.Client.Simple (
2module ProjectM36.Client.Simple (
3− withDBConnection,
3+ simpleConnectProjectM36,
4 withTransaction,
4 withTransaction,
9 AtomType(..),
10 AtomType(..),
10 DBError(..),
11 DBError(..),
@@ -16,71 +17,82 @@ module ProjectM36.Client.Simple (
16 C.DatabaseContextExpr(..),
17 C.DatabaseContextExpr(..),
17 C.RelationalExprBase(..)
18 C.RelationalExprBase(..)
19−-- | A simplified, monad-based client for Project:M36 database access.
20+-- | A simplified client interface for Project:M36 database access.
20import ProjectM36.Error
21import ProjectM36.Error
21import ProjectM36.Base
22import ProjectM36.Base
22import qualified ProjectM36.Client as C
23import qualified ProjectM36.Client as C
23import Control.Monad.Reader
24import Control.Monad.Reader
24import Control.Exception.Base
25import Control.Exception.Base
26−-- | A union of connection and other errors that can be returned from 'withDBConnection'.
27+type DBConn = (C.SessionId, C.Connection)
27−data DBError = ConnError C.ConnectionError |
28− RelError RelationalError
31newtype DB a = DB {runDB :: ReaderT (C.SessionId, C.Connection) IO a}
29newtype DB a = DB {runDB :: ReaderT (C.SessionId, C.Connection) IO a}
32 deriving (Functor, Applicative, Monad)
30 deriving (Functor, Applicative, Monad)
32+newtype TransactionCancelled = TransactionCancelled DBError deriving Show
33+instance Exception TransactionCancelled
34−-- | Opens a connection in the 'DB' monad and closes it after running the monad.
35+-- | A simple alternative to 'connectProjectM36' which includes simple session management
35−withDBConnection :: C.ConnectionInfo -> DB (Either DBError a) -> IO (Either DBError a)
36+simpleConnectProjectM36 :: C.ConnectionInfo -> IO (Either DBError DBConn)
36−withDBConnection connInfo block = bracket (C.connectProjectM36 connInfo) (\eConn -> case eConn of
37+simpleConnectProjectM36 connInfo = do
37− Left _ -> pure (Right ())
38+ eConn <- C.connectProjectM36 connInfo
38− Right conn -> C.close conn >> pure (Right ())) $ \eConn -> do
39 case eConn of
39 case eConn of
40 Left err -> pure (Left (ConnError err))
40 Left err -> pure (Left (ConnError err))
41 Right conn -> do
41 Right conn -> do
42 eSess <- C.createSessionAtHead conn "master"
42 eSess <- C.createSessionAtHead conn "master"
43 case eSess of
43 case eSess of
44− Left err -> pure (Left (RelError err))
46+ pure (Left (RelError err))
45− Right session ->
47+ Right sess -> pure (Right (sess, conn))
49+close :: DBConn -> IO ()
50+close (_ , conn) = C.close conn
52+-- | Runs an IO monad which may include some database updates. If an exception or error occurs, the transaction is rolled back. Otherwise, the transaction is committed.
53+withTransaction :: DBConn -> DB a -> IO (Either DBError a)
54+withTransaction (sess, conn) dbm = do
55+ eHeadName <- C.headName sess conn
57+ Left err -> pure (Left (RelError err))
59+ let successFunc = C.autoMergeToHead sess conn UnionMergeStrategy headName
46− runReaderT (runDB block) (session, conn)
60+ block = runReaderT (runDB dbm) (sess, conn)
47−
61+ handler :: TransactionCancelled -> IO (Either DBError a)
48−wrapClientIO :: (C.SessionId -> C.Connection -> IO (Either RelationalError a)) -> DB (Either DBError a)
62+ handler (TransactionCancelled err) = pure (Left err)
49−wrapClientIO func = DB $ do
63+ handle handler $ do
64+ ret <- C.withTransaction sess conn (block >>= pure . Right) successFunc
66+ Left err -> pure (Left (RelError err))
67+ Right val -> pure (Right val)
69+-- | A union of connection and other errors that can be returned from 'withDBConnection'.
70+data DBError = ConnError C.ConnectionError |
71+ RelError RelationalError |
72+ TransactionRolledBack
75+-- | Execute a 'DatabaseContextExpr' in the 'DB' monad. Database context expressions manipulate the state of the database. In case of an error, the transaction is terminated and the connection's session is rolled back.
76+execute :: C.DatabaseContextExpr -> DB ()
77+execute expr = DB $ do
50 (sess, conn) <- ask
78 (sess, conn) <- ask
51− ret <- liftIO (func sess conn)
79+ ret <- liftIO $ C.executeDatabaseContextExpr sess conn expr
52 case ret of
80 case ret of
53− Left err -> pure (Left (RelError err))
81+ Left err -> liftIO $ cancelTransaction (RelError err)
54− Right val -> pure (Right val)
56−-- | Atomically commits the result of the database context expressions to disk on the server or restores the initial state.
57−withTransaction :: [C.DatabaseContextExpr] -> DB (Either DBError ())
58−withTransaction exprs = do
59− ret <- execute (MultipleExpr exprs)
61− Left err -> pure (Left err)
62− Right _ -> do
82+ Right _ -> pure ()
63− eCurrentHead <- wrapClientIO C.headName
65− Left err -> pure (Left err)
66− Right currentHead -> do
67− --only add a commit if something actually changed
68− eIsDirty <- wrapClientIO C.disconnectedTransactionIsDirty
70− Left err -> pure (Left err)
73− wrapClientIO (\sess conn -> C.autoMergeToHead sess conn C.UnionMergeStrategy currentHead)
77−-- | Execute a 'DatabaseContextExpr' in the 'DB' monad. Database context expressions manipulate the state of the database.
78−execute :: C.DatabaseContextExpr -> DB (Either DBError ())
79−execute expr = wrapClientIO (\sess conn -> C.executeDatabaseContextExpr sess conn expr)
81-- | Run a 'RelationalExpr' query in the 'DB' monad. Relational expressions perform read-only queries against the current database state.
84-- | Run a 'RelationalExpr' query in the 'DB' monad. Relational expressions perform read-only queries against the current database state.
82−query :: C.RelationalExpr -> DB (Either DBError Relation)
85+query :: C.RelationalExpr -> DB Relation
83−query expr = wrapClientIO (\sess conn -> C.executeRelationalExpr sess conn expr)
88+ ret <- lift $ C.executeRelationalExpr sess conn expr
90+ Left err -> liftIO $ cancelTransaction (RelError err) >> error "impossible"
91+ Right rel -> pure rel
93+-- | Unconditionally roll back the current transaction and throw an exception to terminate the execution of the DB monad.
95+rollback = DB $ lift $ cancelTransaction TransactionRolledBack
85−rollback :: DB (Either DBError ())
97+cancelTransaction :: DBError -> IO ()
86−rollback = wrapClientIO C.rollback
98+cancelTransaction err = throwIO (TransactionCancelled err)