{-# 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 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)