hdbi-1.3.0: Database/HDBI/Types.hs
{-# LANGUAGE
TypeFamilies
, CPP
, DeriveDataTypeable
, ExistentialQuantification
, FlexibleContexts
, ScopedTypeVariables
, GeneralizedNewtypeDeriving
, BangPatterns
#-}
{- |
Module : Database.HDBI.Types
Copyright : Copyright (C) 2005-2011 John Goerzen
License : BSD3
Maintainer : Aleksey Uymanov <s9gf4ult@gmail.com>
Stability : experimental
Portability: portable
Types for HDBI.
Written by John Goerzen, jgoerzen\@complete.org
-}
module Database.HDBI.Types
(
-- * Typeclasses
Connection(..)
, Statement(..)
-- * Data types
, Query(..)
, ConnStatus(..)
, ConnWrapper(..)
, StatementStatus(..)
, StmtWrapper(..)
, SqlError(..)
-- * Auxiliary functions
, castConnection
, castStatement
, withTransaction
, withStatement
, runFetch
, runFetchAll
, runFetchOne
) where
#if ! (MIN_VERSION_base(4,6,0))
import Prelude hiding (catch)
#endif
import Control.Applicative ((<$>))
import Control.DeepSeq (NFData(..))
import Control.Exception (Exception(..), SomeException, try, catch, throwIO, bracket)
import Control.Monad (forM_)
import Data.Data (Data(..))
import Data.Monoid (Monoid(..))
import Data.String (IsString(..))
import Data.Typeable
import Database.HDBI.SqlValue (ToRow(..), FromRow(..), FromSql(..), ConvertError(..))
import qualified Data.Sequence as S
import qualified Data.Text.Lazy as TL
-- | Error throwing by driver when database operation fails
data SqlError =
-- | Internal database error
SqlError { seErrorCode :: String -- ^ Low level database-specific error code
, seErrorMsg :: String -- ^ Error description from the database client library
}
-- | Driver-specific operational error
| SqlDriverError { seErrorMsg :: String -- ^ Error description
}
deriving (Eq, Show, Typeable)
instance Exception SqlError
-- | safe newtype wrapper for queries. Just lazy Text inside.
newtype Query = Query { unQuery :: TL.Text -- ^ Unwrap query to lazy Text
}
deriving (Eq, Data, Ord, Read, Show, IsString, Typeable, Monoid, NFData)
-- | Connection status
data ConnStatus = ConnOK -- ^ Successfully connected
| ConnDisconnected -- ^ Successfully disconnected, all
-- statements must be closed at this state
| ConnBad -- ^ Connection is in some bad state
deriving (Typeable, Show, Read, Eq)
-- | Typeclass to abstract the working with connection.
class (Typeable conn, (Statement (ConnStatement conn))) => Connection conn where
-- | Specific statement for specific connection
type ConnStatement conn :: *
-- | Disconnection from the database. Every opened statement must be finished
-- after this method executed.
disconnect :: conn -> IO ()
-- | Explicitly start the transaction. Without starting the transaction you
-- can not commit or rollback it. HDBI does not check if transaction started
-- or not, this is the application's resposibility.
--
-- This is not recomended to use 'start' by hands, use 'withTransaction'
-- instead
begin :: conn -> IO ()
-- | Explicitly commit started transaction. You must 'start' the transaction
-- before 'commit'
--
-- This is not recomended to use 'commit' by hands, use 'withTransaction'
-- instead
commit :: conn -> IO ()
-- | Rollback the transaction's state. You must 'start' the transaction before
-- 'rollback'
--
-- This is not recomended to use 'rollback' by hands, use 'withTransaction'
-- instead
rollback :: conn -> IO ()
-- | Check if current connection is in transaction state. Return True if
-- transaction is started. Each driver implements it with it's own way: some
-- RDBMS has API to check transaction state (like PostgreSQL), some has no
-- (like Sqlite3).
inTransaction :: conn -> IO Bool
-- | Return the current status of connection
connStatus :: conn -> IO ConnStatus
-- | Prepare the statement. Some databases has no feature of preparing
-- statements (PostgreSQL can just prepare named statements), so each driver
-- behaves it's own way.
prepare :: conn -> Query -> IO (ConnStatement conn)
-- | Run query and safely finalize statement after that. Has default
-- implementation through 'execute'.
run :: (ToRow row) => conn -> Query -> row -> IO ()
run conn query row = withStatement conn query
$ \s -> execute s row
{-# INLINEABLE run #-}
-- | Execute query with set of parameters. Has default implementation through
-- 'executeMany'.
runMany :: (ToRow row) => conn -> Query -> [row] -> IO ()
runMany conn query rows = withStatement conn query
$ \s -> executeMany s rows
{-# INLINEABLE runMany #-}
-- | Run raw query. Many databases has an ablility to run a raw queries
-- separated by semicolon. Implementation of this method must use this
-- ability.
-- Has default implementation through 'run'
runRaw :: conn -> Query -> IO ()
runRaw con query = run con query ()
{-# INLINEABLE runRaw #-}
-- | Clone the database connection. Return new connection with the same
-- settings
clone :: conn -> IO conn
-- | The name of the HDBI driver module for this connection. Ideally would be
-- the same as the database name portion of the Cabal package name. For
-- instance, \"sqlite3\" or \"postgresql\". This is the layer that is bound most
-- tightly to HDBI
hdbiDriverName :: conn -> String
-- | Whether or not the current database supports transactions. If False, then
-- 'commit' and 'rollback' should be expected to raise errors.
dbTransactionSupport :: conn -> Bool
-- | Wrapps the specific connection. You can write database-independent code
-- mixing it with database-dependent using 'castConnection' function to cast
-- Wrapper to specific connection type, if you need.
data ConnWrapper = forall conn. Connection conn => ConnWrapper conn
deriving (Typeable)
instance Connection ConnWrapper where
type ConnStatement ConnWrapper = StmtWrapper
disconnect (ConnWrapper conn) = disconnect conn
begin (ConnWrapper conn) = begin conn
commit (ConnWrapper conn) = commit conn
rollback (ConnWrapper conn) = rollback conn
inTransaction (ConnWrapper conn) = inTransaction conn
connStatus (ConnWrapper conn) = connStatus conn
prepare (ConnWrapper conn) str = StmtWrapper <$> prepare conn str
run (ConnWrapper conn) = run conn
runMany (ConnWrapper conn) = runMany conn
runRaw (ConnWrapper conn) = runRaw conn
clone (ConnWrapper conn) = ConnWrapper <$> clone conn
hdbiDriverName (ConnWrapper conn) = hdbiDriverName conn
dbTransactionSupport (ConnWrapper conn) = dbTransactionSupport conn
-- | Cast wrapped connection to the specific connection type using 'cast' of
-- 'Typeable'. You can write database-specific code safely casting wrapped
-- connection to specific type dynamically.
castConnection :: (Connection conn) => ConnWrapper -> Maybe conn
castConnection (ConnWrapper conn) = cast conn
-- | Statement's status returning by function 'statementStatus'.
data StatementStatus = StatementNew -- ^ Newly created statement
| StatementExecuted -- ^ Expression executed, now you can fetch the rows 'Statement'
| StatementFetched -- ^ Fetching is done, no more rows can be queried
| StatementFinished -- ^ Finished, no more actions with this statement
deriving (Typeable, Show, Read, Eq)
-- | Statement prepared on database side or just in memory
class (Typeable stmt) => Statement stmt where
-- | Execute single query with parameters. In query each parameter must be
-- replaced with ''?'' placeholder. This rule is true for every database, even
-- for PostgreSQL which uses placeholders like ''$1''. Application must ensure
-- that the count of placeholders is equal to count of parameter, it is likely
-- cause an error if it is not.
execute :: (ToRow row) => stmt -> row -> IO ()
-- | Execute one query many times with a list of paramters. Has default
-- implementation through 'execute'.
executeMany :: (ToRow row) => stmt -> [row] -> IO ()
executeMany stmt rows = forM_ rows $ \row -> do
execute stmt row
reset stmt
-- | Return the current statement's status.
statementStatus :: stmt -> IO StatementStatus
-- | Finish statement and remove database-specific pointer. No any actions may
-- be proceeded after closing the statement, excpet 'statementStatus' which
-- will return 'StatementFinished' and 'reset'.
finish :: stmt -> IO ()
-- | Reset statement to it's initial state, just like after 'prepare'.
reset :: stmt -> IO ()
-- | Fetch next row from the executed statement. Return Nothing when there is
-- no more results acceptable. Each call return next row from the result.
--
-- UPDATE INSERT and DELETE queries will likely return Nothing.
--
-- NOTE: You still need to explicitly finish the statement after receiving
-- Nothing, unlike with old HDBC interface.
fetch :: (FromRow row) => stmt -> IO (Maybe row)
-- | Optional method to strictly fetch all rows from statement. Has default
-- implementation through 'fetch'.
fetchAll :: (FromRow row) => stmt -> IO (S.Seq row)
fetchAll stmt = fetchAll' S.empty
where
fetchAll' !s = do
r <- fetch stmt
case r of
Nothing -> return s
Just row -> fetchAll' $ s S.|> row
-- | Return list of column names of the result.
getColumnNames :: stmt -> IO [TL.Text]
-- | Return the number of columns representing the result. Has default
-- implementation through 'getColumnNames'
getColumnsCount :: stmt -> IO Int
getColumnsCount stmt = length <$> getColumnNames stmt
-- | Return the original query the statement was prepared from.
originalQuery :: stmt -> Query
-- | Wrapper around some specific 'Statement' instance to write
-- database-independent code
data StmtWrapper = forall stmt. Statement stmt => StmtWrapper stmt
deriving (Typeable)
instance Statement StmtWrapper where
execute (StmtWrapper stmt) = execute stmt
executeMany (StmtWrapper stmt) = executeMany stmt
statementStatus (StmtWrapper stmt) = statementStatus stmt
finish (StmtWrapper stmt) = finish stmt
reset (StmtWrapper stmt) = reset stmt
fetch (StmtWrapper stmt) = fetch stmt
fetchAll (StmtWrapper stmt) = fetchAll stmt
getColumnNames (StmtWrapper stmt) = getColumnNames stmt
getColumnsCount (StmtWrapper stmt) = getColumnsCount stmt
originalQuery (StmtWrapper stmt) = originalQuery stmt
-- | Cast wrapped statement to specific type. You can write database-specific
-- code safely casting wrapped statement to specific type dynamically.
castStatement :: (Statement stmt) => StmtWrapper -> Maybe stmt
castStatement (StmtWrapper stmt) = cast stmt
{- | Execute some code. If any uncaught exception occurs, run
'rollback' and re-raise it. Otherwise, run 'commit' and return.
This function, therefore, encapsulates the logical property that a transaction
is all about: all or nothing.
The 'Connection' object passed in is passed directly to the specified function
as a convenience.
This function traps /all/ uncaught exceptions, not just 'SqlError'. Therefore,
you will get a rollback for any exception that you don't handle. That's
probably what you want anyway.
If there was an error while running 'rollback', this error will not be
reported since the original exception will be propogated back. (You'd probably
like to know about the root cause for all of this anyway.) Feedback
on this behavior is solicited.
-}
withTransaction :: Connection conn => conn -> IO a -> IO a
withTransaction conn func = do
begin conn
r <- try func
case r of
Right x -> do
commit conn
return x
Left (e :: SomeException) -> do
catch (rollback conn) (\(_ :: SomeException) -> return ())
throwIO e
{-| Create statement and execute monadic action using
it. Safely finalize Statement after action is done.
-}
withStatement :: (Connection conn, Statement stmt, stmt ~ (ConnStatement conn))
=> conn -- ^ Connection
-> Query -- ^ Query string
-> (stmt -> IO a) -- ^ Action at the statement
-> IO a
withStatement conn query = bracket
(prepare conn query)
finish
{-# INLINEABLE withStatement #-}
-- | Run query and return first row
runFetch :: (Connection con, ToRow params, FromRow row)
=> con -> Query -> params -> IO (Maybe row)
runFetch con query params = withStatement con query $ \stmt -> do
execute stmt params
fetch stmt
runFetchAll :: (Connection con, ToRow params, FromRow row)
=> con -> Query -> params -> IO (S.Seq row)
runFetchAll con query params = withStatement con query $ \stmt -> do
execute stmt params
fetchAll stmt
runFetchOne :: (Connection con, ToRow params, FromSql col)
=> con -> Query -> params -> IO (Maybe col)
runFetchOne con query params = withStatement con query $ \stmt -> do
execute stmt params
r <- fetch stmt
case r of
Nothing -> return Nothing
(Just [col]) -> case safeFromSql col of
Left e -> throwIO e
Right row -> return $ Just row
(Just x) -> throwIO $ ConvertError $ "Query \"" ++ (TL.unpack $ unQuery query)
++ "\" should return exactly ONE column, but returned " ++ (show $ length x)