pgstream-0.1.0.3: src/Database/PostgreSQL/Stream.hs
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Database.PostgreSQL.Stream (
-- ** SQL query ( result set )
query,
query_,
-- ** SQL query ( no result )
execute,
execute_,
-- ** SQL query ( cursor )
stream,
stream_,
-- ** Transacations
commit,
rollback,
-- ** Database pool
PoolSettings(..),
pgPool,
withPgConnection,
-- ** LibPQ Rexports
PQ.Connection,
PQ.ExecStatus(..),
PQ.ConnStatus(..),
-- ** Connection initialization
ConnSettings(..),
connect,
connect_alt,
-- ** SQL quasiquoter
sql,
-- ** SQL formatting
fmtSQL,
fmtQuery,
printSQL,
--ver,
module Database.PostgreSQL.Stream.Types
) where
import Database.PostgreSQL.Stream.Types
import Database.PostgreSQL.Stream.FromRow
import Database.PostgreSQL.Stream.QueryBuilder
import Database.PostgreSQL.Stream.Connection
import qualified PostgreSQLBinary.Decoder as PD
import qualified Database.PostgreSQL.LibPQ as PQ
import Data.Int
import Data.Text
import Data.Pool
import Data.Monoid
import Data.ByteString (ByteString)
import Data.ByteString.Char8 as B8
import qualified Data.Vector.Storable as V
import Data.UUID.V4(nextRandom)
import Data.UUID(toASCIIBytes, toWords)
import Control.Exception as E
import Control.Monad.Trans
import Control.Applicative
import Control.Monad.Trans.Resource (ResourceT, MonadBaseControl)
import qualified Data.Conduit as C
import qualified Data.Conduit.List as CL
import Data.Version (showVersion)
import qualified Paths_pgstream as Paths
ver :: String
ver = showVersion Paths.version
-------------------------------------------------------------------------------
-- Query
-------------------------------------------------------------------------------
-- Execute SQL query with arguments.
query :: (FromRow r, ToSQL q) => PQ.Connection -> Query -> q -> IO [r]
query conn q args = do
-- Formatted query
let query = fmtQuery q args
-- Execute the query
result <- PQ.execParams conn query [] PQ.Binary
case result of
Nothing -> throw (QueryError "Query execution error." q)
Just pqres -> do
status <- PQ.resultStatus pqres
onError pqres q
parseRows q pqres
-- Execute SQL query without arguments.
query_ :: (FromRow r) => PQ.Connection -> Query -> IO [r]
query_ conn q = do
-- Execute the query
result <- PQ.execParams conn (fromQuery q) [] PQ.Binary
case result of
Nothing -> throw (QueryError "Query execution error." q)
Just pqres -> do
status <- PQ.resultStatus pqres
onError pqres q
parseRows q pqres
-------------------------------------------------------------------------------
-- Execute
-------------------------------------------------------------------------------
execute :: (ToSQL q) => PQ.Connection -> Query -> q -> IO PQ.ExecStatus
execute conn q args = do
-- Formatted query
let query = fmtQuery q args
-- Execute the query
result <- PQ.execParams conn query [] PQ.Binary
case result of
Nothing -> throw (QueryError "Query execution error." q)
Just pqres -> do
status <- PQ.resultStatus pqres
onError pqres q
return status
execute_ :: PQ.Connection -> Query -> IO PQ.ExecStatus
execute_ conn q = do
-- Execute the query
result <- PQ.execParams conn (fromQuery q) [] PQ.Binary
case result of
Nothing -> throw (QueryError "Query execution error." q)
Just pqres -> do
status <- PQ.resultStatus pqres
onError pqres q
return status
-------------------------------------------------------------------------------
-- Error Handling
-------------------------------------------------------------------------------
onError :: PQ.Result -> Query -> IO ()
onError pqres q = do
status <- PQ.resultStatus pqres
case status of
PQ.FatalError -> do
res <- PQ.resultErrorMessage pqres
case res of
Nothing -> return () -- should never happen
Just err -> throw (QueryError (B8.unpack err) q)
_ -> return ()
-------------------------------------------------------------------------------
-- Transaction
-------------------------------------------------------------------------------
data IsolationLevel
= DefaultIsolationLevel
| ReadCommitted
| RepeatableRead
| Serializable
deriving (Show, Eq, Ord, Enum, Bounded)
data ReadWriteMode
= DefaultReadWriteMode
| ReadWrite
| ReadOnly
deriving (Show, Eq, Ord, Enum, Bounded)
data TransactionMode = TransactionMode
{ isolationLevel :: !IsolationLevel
, readWriteMode :: !ReadWriteMode
} deriving (Show, Eq)
beginMode :: TransactionMode -> PQ.Connection -> IO PQ.ExecStatus
beginMode mode conn = do
let begin_query = (Query (mconcat ["BEGIN", isolevel, readmode, ";"]))
execute_ conn begin_query
where
isolevel =
case isolationLevel mode of
DefaultIsolationLevel -> ""
ReadCommitted -> " ISOLATION LEVEL READ COMMITTED"
RepeatableRead -> " ISOLATION LEVEL REPEATABLE READ"
Serializable -> " ISOLATION LEVEL SERIALIZABLE"
readmode =
case readWriteMode mode of
DefaultReadWriteMode -> ""
ReadWrite -> " READ WRITE"
ReadOnly -> " READ ONLY"
defaultTransactionMode :: TransactionMode
defaultTransactionMode = TransactionMode
defaultIsolationLevel
defaultReadWriteMode
defaultIsolationLevel ::IsolationLevel
defaultIsolationLevel = DefaultIsolationLevel
defaultReadWriteMode :: ReadWriteMode
defaultReadWriteMode = DefaultReadWriteMode
-- | Rollback a transaction.
rollback :: PQ.Connection -> IO ()
rollback conn = execute_ conn "ABORT" >> return ()
-- | Commit a transaction.
commit :: PQ.Connection -> IO ()
commit conn = execute_ conn "COMMIT" >> return ()
-- | Begin a transaction.
begin :: PQ.Connection -> IO PQ.ExecStatus
begin = beginMode defaultTransactionMode
withTransactionMode :: TransactionMode -> PQ.Connection -> IO a -> IO a
withTransactionMode mode conn act =
mask $ \restore -> do
beginMode mode conn
r <- restore act `E.onException` rollback conn
commit conn
return r
-------------------------------------------------------------------------------
-- Streaming Queries
-------------------------------------------------------------------------------
newCursor :: IO (Identifier)
newCursor = do
uid <- nextRandom
let (a,b,c,d) = toWords uid
let bshow = B8.pack . show
return $ Identifier ("cursor" <> bshow a <> bshow b <> bshow c <> bshow d)
stream :: (FromRow r, ToSQL a, MonadBaseControl IO m, MonadIO m) =>
PQ.Connection -- ^ Connection
-> Query -- ^ Query
-> a -- ^ Query arguments
-> Int -- ^ Batch size
-> C.Source m [r] -- ^ Source conduit
stream conn q args n = do
-- Generate a new cursor from a uuid
cursor_name <- liftIO $ newCursor
liftIO $ beginMode defaultTransactionMode conn
let subsql = fmtQuery q args
let cursor_query = Query (mconcat [ "DECLARE {1} NO SCROLL CURSOR FOR ", subsql ])
-- Execute the cursor setup
liftIO $ execute conn cursor_query (Only cursor_name)
let fetch_cursor = [sql|FETCH FORWARD {1} FROM {2};|]
res <- C.tryC (loop conn fetch_cursor cursor_name n)
case res of
Left (err :: QueryError) -> liftIO $ do
-- On SQL exception, rollback the transaction abort the conduit
rollback conn
throwIO err
-- On SQL success, commit
Right _ -> liftIO $ commit conn
where
loop conn q cursor_name n = do
res <- liftIO $ query conn q (n, cursor_name)
case res of
[] -> return ()
_ -> C.yield res >> loop conn q cursor_name n
stream_ :: (FromRow r, MonadBaseControl IO m, MonadIO m) =>
PQ.Connection -- ^ Connection
-> Query -- ^ Query
-> Int -- ^ Batch size
-> C.Source m [r] -- ^ Source conduit
stream_ conn q n = stream conn q () n
printSQL :: Query -> IO ()
printSQL (Query bs) = B8.putStrLn bs