------------------------------------------------------------------------------
--- Library for accessing and storing data in databases.
--- The contents of a database is represented in this library
--- as dynamic predicates that are defined by facts than can change over
--- time and can be persistently stored.
--- All functions in this library distinguishes between queries that
--- access the database and transactions that manipulates data
--- in the database. Transactions have a monadic structure.
--- Both queries and transactions can be executed as I/O actions.
--- However, arbitrary I/O actions cannot be embedded in transactions.
---
--- A dynamic predicate p
with arguments of type
--- t1,...,tn
must be declared by:
---
--- p :: t1 -> ... -> tn -> Dynamic
--- p = dynamic
---
---
--- A dynamic predicate where all facts should be persistently stored
--- in the directory DIR
must be declared by:
---
--- p :: t1 -> ... -> tn -> Dynamic
--- p = persistent "file:DIR"
---
--- @author Michael Hanus
--- @version August 2011
--- @category database
------------------------------------------------------------------------------
module Database(Dynamic,dynamic,persistent,(<>),(|&>),(|>),
Query,queryOne,queryAll,queryOneWithDefault,queryJustOne,
dynamicExists,transformQ,runQ,
Transaction,TError(..),TErrorKind(..),showTError,
addDB,deleteDB,getDB,
(|>>),(|>>=),returnT,doneT,errorT,failT,
sequenceT,sequenceT_,mapT,mapT_,runT,runJustT,runTNA)
where
import Dynamic
import Global -- to store transaction errors
infixl 1 |>>, |>>=
------------------------------------------------------------------------------
-- Database queries:
--- Abstract datatype to represent database queries.
data Query a = QueryDB (IO a)
--- A database query that returns all answers to an abstraction on a
--- dynamic expression.
queryAll :: (a -> Dynamic) -> Query [a]
queryAll dynq = QueryDB (getDynamicSolutions dynq)
--- A database query that returns a single answer to an abstraction on a
--- dynamic expression. It returns Nothing if no answer exists.
queryOne :: (a -> Dynamic) -> Query (Maybe a)
queryOne dynq = QueryDB (getDynamicSolution dynq)
--- A database query that returns a single answer to an abstraction on a
--- dynamic expression. It returns the first argument if no answer exists.
queryOneWithDefault :: a -> (a -> Dynamic) -> Query a
queryOneWithDefault d dynq = transformQ (maybe d id) (queryOne dynq)
--- A database query that returns a single answer to an abstraction on a
--- dynamic expression. It fails if no answer exists.
queryJustOne :: (a -> Dynamic) -> Query a
queryJustOne = queryOneWithDefault failed
--- A database query that returns True if there exists the argument facts
--- (without free variables!) and False, otherwise.
dynamicExists :: Dynamic -> Query Bool
dynamicExists dyn = QueryDB (isKnown dyn)
--- Transforms a database query from one result type to another
--- according to a given mapping.
transformQ :: (a -> b) -> Query a -> Query b
transformQ f (QueryDB a) = QueryDB (a >>= return . f)
--- Executes a database query on the current state of dynamic predicates.
--- If other processes made changes to persistent predicates,
--- these changes are read and made visible to the currently running program.
runQ :: Query a -> IO a
runQ (QueryDB q) = q
------------------------------------------------------------------------------
-- Transactions:
--- The type of errors that might occur during a transaction.
data TError = TError TErrorKind String
--- The various kinds of transaction errors.
data TErrorKind = KeyNotExistsError
| NoRelationshipError
| DuplicateKeyError
| KeyRequiredError
| UniqueError
| MinError
| MaxError
| UserDefinedError
| ExecutionError
--- Transforms a transaction error into a string.
showTError :: TError -> String
showTError (TError k s) = "Transaction error " ++ show k ++ ": " ++ s
--- Abstract datatype for representing transactions.
data Transaction a = TransDB (IO (TransResult a))
-- Internal type for representing the result of a transaction.
data TransResult a = OK a
| Error TError
--- Adds new facts (without free variables!) about dynamic predicates.
--- Conditional dynamics are added only if the condition holds.
addDB :: Dynamic -> Transaction ()
addDB dyn = TransDB (Dynamic.assert dyn >> return (OK ()))
--- Deletes facts (without free variables!) about dynamic predicates.
--- Conditional dynamics are deleted only if the condition holds.
deleteDB :: Dynamic -> Transaction ()
deleteDB dyn = TransDB (Dynamic.retract dyn >> return (OK ()))
--- Returns the result of a database query in a transaction.
getDB :: Query a -> Transaction a
getDB (QueryDB q) = TransDB (q >>= \qresult -> return (OK qresult))
--- The empty transaction that directly returns its argument.
returnT :: a -> Transaction a
returnT x = TransDB (return (OK x))
--- The empty transaction that returns nothing.
doneT :: Transaction ()
doneT = returnT ()
--- Abort a transaction with a specific transaction error.
errorT :: TError -> Transaction _
errorT e = TransDB (return (Error e))
--- Abort a transaction with a general error message.
failT :: String -> Transaction _
failT s = errorT (TError UserDefinedError s)
--- Sequential composition of transactions.
--- @param a - a transaction
--- @param fa - a function from a value into a transaction
--- @return a transaction that first performs a
--- (yielding result r
)
--- and then performs (fa r)
(|>>=) :: Transaction a -> (a -> Transaction b) -> Transaction b
(TransDB t1) |>>= ft = TransDB $ do
r1 <- t1
case r1 of
Error e -> return (Error e)
OK t1value -> let TransDB t2 = ft t1value in t2
--- Sequential composition of transactions.
--- @param a1 - a transaction
--- @param a2 - a transaction
--- @return a transaction that first performs a1 and then a2
(|>>) :: Transaction _ -> Transaction a -> Transaction a
t1 |>> t2 = t1 |>>= \_ -> t2
--- Executes a sequence of transactions and collects all results in a list.
sequenceT :: [Transaction a] -> Transaction [a]
sequenceT [] = returnT []
sequenceT (t:ts) = t |>>= \x -> sequenceT ts |>>= \xs -> returnT (x:xs)
--- Executes a sequence of transactions and ignores the results.
sequenceT_ :: [Transaction _] -> Transaction ()
sequenceT_ = foldr (|>>) doneT
--- Maps a transaction function on a list of elements.
--- The results of all transactions are collected in a list.
mapT :: (a -> Transaction b) -> [a] -> Transaction [b]
mapT f = sequenceT . map f
--- Maps a transaction function on a list of elements.
--- The results of all transactions are ignored.
mapT_ :: (a -> Transaction _) -> [a] -> Transaction ()
mapT_ f = sequenceT_ . map f
--- Executes a possibly composed transaction on the current state
--- of dynamic predicates as a single transaction.
---
--- Before the transaction is executed, the access to all persistent
--- predicates is locked (i.e., no other process can perform a
--- transaction in parallel).
--- After the successful transaction, the access is unlocked so that
--- the updates performed in this transaction become persistent and
--- visible to other processes.
--- Otherwise (i.e., in case of a failure or abort of the transaction),
--- the changes of the transaction to persistent predicates are
--- ignored and Nothing is returned.
---
--- In general, a transaction should terminate and all failures inside
--- a transaction should be handled (execept for an explicit failT
--- that leads to an abort of the transaction).
--- If a transaction is externally interrupted (e.g., by killing the process),
--- some locks might never be removed. However, they
--- can be explicitly removed by deleting the corresponding lock files
--- reported at startup time.
runT :: Transaction a -> IO (Either a TError)
runT t = do
writeGlobal currentTransError execError
etr <- Dynamic.transactionWithErrorCatch
(let TransDB trans = t in trans >>= \tresult ->
case tresult of
OK _ -> return tresult
Error e -> writeGlobal currentTransError e >> failed)
either (return . transformResult)
(\re -> readGlobal currentTransError >>= \e ->
return (Right (if e==execError
then TError ExecutionError (showError re)
else e)))
etr
where
transformResult (OK x) = Left x
transformResult (Error e) = Right e
--- Executes a possibly composed transaction on the current state
--- of dynamic predicates as a single transaction.
--- Similarly to runT
but a run-time error is raised
--- if the execution of the transaction fails.
runJustT :: Transaction a -> IO a
runJustT t =
runT t >>=
return . either id
(\e -> error ("Transaction failed: " ++ showTError e))
--- Executes a possibly composed transaction as a Non-Atomic(!)
--- sequence of its individual database updates.
--- Thus, the argument is not executed as a single transaction
--- in contrast to runT
, i.e., no predicates are
--- locked and individual updates are not undone in case of a
--- transaction error.
--- This operation could be applied to execute a composed transaction
--- without the overhead caused by (the current implementation of)
--- transactions if one is sure that locking is not necessary
--- (e.g., if the transaction contains only database reads and
--- transaction error raising).
runTNA :: Transaction a -> IO (Either a TError)
runTNA t = do
writeGlobal currentTransError execError
etr <- safeExecIO (let TransDB trans = t in trans >>= \tresult ->
case tresult of
OK _ -> return tresult
Error e -> writeGlobal currentTransError e >> failed)
either (return . transformResult)
(\re -> readGlobal currentTransError >>= \e ->
return (Right (if e==execError
then TError ExecutionError (showError re)
else e)))
etr
where
transformResult (OK x) = Left x
transformResult (Error e) = Right e
safeExecIO :: IO a -> IO (Either a IOError)
safeExecIO action = catch (action >>= return . Left) (return . Right)
-- Global entity to store the transaction error during the execution of
-- the transaction.
currentTransError :: Global TError
currentTransError = global execError Temporary
execError :: TError
execError = TError ExecutionError "run-time error during transaction execution"