git.iko.soy/typeable/postgresql-simple
on master help sign in
Merge pull request #213 from BardurArantsson/wip-add-cursor

Split Cursor code into separate module

commitf92ff66f12495923048b7c8c4ab4b2ee619b1f08 authorLeon P Smith <leon.p.smith+github@gmail.com> dateMon 15 May 2017, 04:44 (9 years ago) parent7d07d3e2ed060f
7 files changed+212−113
.gitignore+1 cabal.project+1 postgresql-simple.cabal+2 src/Database/PostgreSQL/Simple.hs+15−111 src/Database/PostgreSQL/Simple/Copy.hs+5−2 src/Database/PostgreSQL/Simple/Cursor.hs+99 src/Database/PostgreSQL/Simple/Internal/PQResultUtils.hs+89
.gitignore+1
@@ -11,3 +11,4 @@ dist/
1111.cabal-sandbox/
1212.stack-work/
1313cabal.sandbox.config
14+/dist-newstyle
cabal.projectnew file+1
@@ -0,0 +1 @@
1+packages: .
postgresql-simple.cabal+2
@@ -24,6 +24,7 @@ Library
2424 Database.PostgreSQL.Simple
2525 Database.PostgreSQL.Simple.Arrays
2626 Database.PostgreSQL.Simple.Copy
27+ Database.PostgreSQL.Simple.Cursor
2728 Database.PostgreSQL.Simple.FromField
2829 Database.PostgreSQL.Simple.FromRow
2930 Database.PostgreSQL.Simple.LargeObjects
@@ -49,6 +50,7 @@ Library
4950 Other-modules:
5051 Database.PostgreSQL.Simple.Compat
5152 Database.PostgreSQL.Simple.HStore.Implementation
53+ Database.PostgreSQL.Simple.Internal.PQResultUtils
5254 Database.PostgreSQL.Simple.Time.Implementation
5355 Database.PostgreSQL.Simple.Time.Internal.Parser
5456 Database.PostgreSQL.Simple.Time.Internal.Printer
src/Database/PostgreSQL/Simple.hs+15−111
@@ -118,30 +118,26 @@ module Database.PostgreSQL.Simple
118118 , formatQuery
119119 ) where
120120
121−import Data.ByteString.Builder
122− ( Builder, byteString, char8, intDec )
121+import Data.ByteString.Builder (Builder, byteString, char8)
123122import Control.Applicative ((<$>))
124123import Control.Exception as E
125−import Control.Monad (unless)
126124import Data.ByteString (ByteString)
127125import Data.Int (Int64)
128126import Data.List (intersperse)
129127import Data.Monoid (mconcat)
130−import Database.PostgreSQL.Simple.Compat ( (<>), toByteString )
128+import Database.PostgreSQL.Simple.Compat ((<>), toByteString)
129+import Database.PostgreSQL.Simple.Cursor
131130import Database.PostgreSQL.Simple.FromField (ResultError(..))
132131import Database.PostgreSQL.Simple.FromRow (FromRow(..))
133−import Database.PostgreSQL.Simple.Ok
134132import Database.PostgreSQL.Simple.ToField (Action(..))
135133import Database.PostgreSQL.Simple.ToRow (ToRow(..))
136134import Database.PostgreSQL.Simple.Types
137135 ( Binary(..), In(..), Only(..), Query(..), (:.)(..) )
138136import Database.PostgreSQL.Simple.Internal as Base
137+import Database.PostgreSQL.Simple.Internal.PQResultUtils
139138import Database.PostgreSQL.Simple.Transaction
140−import Database.PostgreSQL.Simple.TypeInfo
141139import qualified Database.PostgreSQL.LibPQ as PQ
142140import qualified Data.ByteString.Char8 as B
143−import Control.Monad.Trans.Reader
144−import Control.Monad.Trans.State.Strict
145141
146142
147143-- | Format a query string.
@@ -562,39 +558,17 @@ doFold FoldOptions{..} parser conn _template q a0 f = do
562558 PQ.TransUnknown -> fail "foldWithOpts FIXME: PQ.TransUnknown"
563559 -- Not sure what this means.
564560 where
565− declare = do
566− name <- newTempName conn
567− _ <- execute_ conn $ mconcat
568− [ "DECLARE ", name, " NO SCROLL CURSOR FOR ", q ]
569− return name
570− close name =
571− (execute_ conn ("CLOSE " <> name) >> return ()) `E.catch` \ex ->
572− -- Don't throw exception if CLOSE failed because the transaction is
573− -- aborted. Otherwise, it will throw away the original error.
574− unless (isFailedTransactionError ex) $ throwIO ex
575−
576− go = bracket declare close $ \(Query name) ->
577− let q = toByteString (byteString "FETCH FORWARD "
578− <> intDec chunkSize
579− <> byteString " FROM "
580− <> byteString name
581− )
582− loop a = do
583− result <- exec conn q
584− status <- PQ.resultStatus result
585− case status of
586− PQ.TuplesOk -> do
587− nrows <- PQ.ntuples result
588− ncols <- PQ.nfields result
589− if nrows > 0
590− then do
591− let inner a row = do
592− x <- getRowWith parser row ncols conn result
593− f a x
594− foldM' inner a 0 (nrows - 1) >>= loop
595− else return a
596− _ -> throwResultError "fold" result status
597− in loop a0
561+ declare =
562+ declareCursor conn q
563+ fetch cursor a =
564+ foldForwardWithParser cursor parser chunkSize f a
565+
566+ go = bracket declare closeCursor $ \cursor ->
567+ let loop a = fetch cursor a >>=
568+ \r -> case r of
569+ Left a -> return a
570+ Right a -> loop a
571+ in loop a0
598572
599573-- FIXME: choose the Automatic chunkSize more intelligently
600574-- One possibility is to use the type of the results, although this
@@ -644,76 +618,6 @@ forEachWith_ :: RowParser r
644618forEachWith_ parser conn template = foldWith_ parser conn template () . const
645619{-# INLINE forEachWith_ #-}
646620
647−forM' :: (Ord n, Num n) => n -> n -> (n -> IO a) -> IO [a]
648−forM' lo hi m = loop hi []
649− where
650− loop !n !as
651− | n < lo = return as
652− | otherwise = do
653− a <- m n
654− loop (n-1) (a:as)
655−{-# INLINE forM' #-}
656−
657−foldM' :: (Ord n, Num n) => (a -> n -> IO a) -> a -> n -> n -> IO a
658−foldM' f a lo hi = loop a lo
659− where
660− loop a !n
661− | n > hi = return a
662− | otherwise = do
663− a' <- f a n
664− loop a' (n+1)
665−{-# INLINE foldM' #-}
666−
667−finishQueryWith :: RowParser r -> Connection -> Query -> PQ.Result -> IO [r]
668−finishQueryWith parser conn q result = do
669− status <- PQ.resultStatus result
670− case status of
671− PQ.EmptyQuery ->
672− throwIO $ QueryError "query: Empty query" q
673− PQ.CommandOk ->
674− throwIO $ QueryError "query resulted in a command response" q
675− PQ.TuplesOk -> do
676− nrows <- PQ.ntuples result
677− ncols <- PQ.nfields result
678− forM' 0 (nrows-1) $ \row ->
679− getRowWith parser row ncols conn result
680− PQ.CopyOut ->
681− throwIO $ QueryError "query: COPY TO is not supported" q
682− PQ.CopyIn ->
683− throwIO $ QueryError "query: COPY FROM is not supported" q
684− PQ.BadResponse -> throwResultError "query" result status
685− PQ.NonfatalError -> throwResultError "query" result status
686− PQ.FatalError -> throwResultError "query" result status
687−
688−getRowWith :: RowParser r -> PQ.Row -> PQ.Column -> Connection -> PQ.Result -> IO r
689−getRowWith parser row ncols conn result = do
690− let rw = Row row result
691− let unCol (PQ.Col x) = fromIntegral x :: Int
692− okvc <- runConversion (runStateT (runReaderT (unRP parser) rw) 0) conn
693− case okvc of
694− Ok (val,col) | col == ncols -> return val
695− | otherwise -> do
696− vals <- forM' 0 (ncols-1) $ \c -> do
697− tinfo <- getTypeInfo conn =<< PQ.ftype result c
698− v <- PQ.getvalue result row c
699− return ( tinfo
700− , fmap ellipsis v )
701− throw (ConversionFailed
702− (show (unCol ncols) ++ " values: " ++ show vals)
703− Nothing
704− ""
705− (show (unCol col) ++ " slots in target type")
706− "mismatch between number of columns to \
707− \convert and number in target type")
708− Errors [] -> throwIO $ ConversionFailed "" Nothing "" "" "unknown error"
709− Errors [x] -> throwIO x
710− Errors xs -> throwIO $ ManyErrors xs
711−
712−ellipsis :: ByteString -> ByteString
713−ellipsis bs
714− | B.length bs > 15 = B.take 10 bs `B.append` "[...]"
715− | otherwise = bs
716−
717621
718622-- $use
719623--
src/Database/PostgreSQL/Simple/Copy.hs+5−2
@@ -75,15 +75,18 @@ doCopy :: B.ByteString -> Connection -> Query -> B.ByteString -> IO ()
7575doCopy funcName conn template q = do
7676 result <- exec conn q
7777 status <- PQ.resultStatus result
78− let err = throwIO $ QueryError
79− (B.unpack funcName ++ " " ++ show status)
78+ let errMsg msg = throwIO $ QueryError
79+ (B.unpack funcName ++ " " ++ msg)
8080 template
81+ let err = errMsg $ show status
8182 case status of
8283 PQ.EmptyQuery -> err
8384 PQ.CommandOk -> err
8485 PQ.TuplesOk -> err
8586 PQ.CopyOut -> return ()
8687 PQ.CopyIn -> return ()
88+ PQ.CopyBoth -> errMsg "COPY BOTH is not supported"
89+ PQ.SingleTuple -> errMsg "single-row mode is not supported"
8790 PQ.BadResponse -> throwResultError funcName result status
8891 PQ.NonfatalError -> throwResultError funcName result status
8992 PQ.FatalError -> throwResultError funcName result status
src/Database/PostgreSQL/Simple/Cursor.hsnew file+99
@@ -0,0 +1,99 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+------------------------------------------------------------------------------
4+-- |
5+-- Module: Database.PostgreSQL.Simple.Cursor
6+-- Copyright: (c) 2011 MailRank, Inc.
7+-- (c) 2011-2012 Leon P Smith
8+-- (c) 2017 Bardur Arantsson
9+-- License: BSD3
10+-- Maintainer: Leon P Smith <leon@melding-monads.com>
11+--
12+------------------------------------------------------------------------------
13+
14+module Database.PostgreSQL.Simple.Cursor
15+ (
16+ -- * Types
17+ Cursor
18+ -- * Cursor management
19+ , declareCursor
20+ , closeCursor
21+ -- * Folding over rows from a cursor
22+ , foldForward
23+ , foldForwardWithParser
24+ ) where
25+
26+import Data.ByteString.Builder (intDec)
27+import Control.Applicative ((<$>))
28+import Control.Exception as E
29+import Control.Monad (unless, void)
30+import Data.Monoid (mconcat)
31+import Database.PostgreSQL.Simple.Compat ((<>), toByteString)
32+import Database.PostgreSQL.Simple.FromRow (FromRow(..))
33+import Database.PostgreSQL.Simple.Types (Query(..))
34+import Database.PostgreSQL.Simple.Internal as Base
35+import Database.PostgreSQL.Simple.Internal.PQResultUtils
36+import Database.PostgreSQL.Simple.Transaction
37+import qualified Database.PostgreSQL.LibPQ as PQ
38+
39+-- | Cursor within a transaction.
40+data Cursor = Cursor !Query !Connection
41+
42+-- | Declare a temporary cursor. The cursor is given a
43+-- unique name for the given connection.
44+declareCursor :: Connection -> Query -> IO Cursor
45+declareCursor conn q = do
46+ name <- newTempName conn
47+ void $ execute_ conn $ mconcat ["DECLARE ", name, " NO SCROLL CURSOR FOR ", q]
48+ return $ Cursor name conn
49+
50+-- | Close the given cursor.
51+closeCursor :: Cursor -> IO ()
52+closeCursor (Cursor name conn) =
53+ (void $ execute_ conn ("CLOSE " <> name)) `E.catch` \ex ->
54+ -- Don't throw exception if CLOSE failed because the transaction is
55+ -- aborted. Otherwise, it will throw away the original error.
56+ unless (isFailedTransactionError ex) $ throwIO ex
57+
58+-- | Fold over a chunk of rows from the given cursor, calling the
59+-- supplied fold-like function on each row as it is received. In case
60+-- the cursor is exhausted, a 'Left' value is returned, otherwise a
61+-- 'Right' value is returned.
62+foldForwardWithParser :: Cursor -> RowParser r -> Int -> (a -> r -> IO a) -> a -> IO (Either a a)
63+foldForwardWithParser (Cursor name conn) parser chunkSize f a0 = do
64+ let q = "FETCH FORWARD "
65+ <> (toByteString $ intDec chunkSize)
66+ <> " FROM "
67+ <> fromQuery name
68+ result <- exec conn q
69+ status <- PQ.resultStatus result
70+ case status of
71+ PQ.TuplesOk -> do
72+ nrows <- PQ.ntuples result
73+ ncols <- PQ.nfields result
74+ if nrows > 0
75+ then do
76+ let inner a row = do
77+ x <- getRowWith parser row ncols conn result
78+ f a x
79+ Right <$> foldM' inner a0 0 (nrows - 1)
80+ else
81+ return $ Left a0
82+ _ -> throwResultError "foldForwardWithParser" result status
83+
84+-- | Fold over a chunk of rows, calling the supplied fold-like function
85+-- on each row as it is received. In case the cursor is exhausted,
86+-- a 'Left' value is returned, otherwise a 'Right' value is returned.
87+foldForward :: FromRow r => Cursor -> Int -> (a -> r -> IO a) -> a -> IO (Either a a)
88+foldForward cursor = foldForwardWithParser cursor fromRow
89+
90+
91+foldM' :: (Ord n, Num n) => (a -> n -> IO a) -> a -> n -> n -> IO a
92+foldM' f a lo hi = loop a lo
93+ where
94+ loop a !n
95+ | n > hi = return a
96+ | otherwise = do
97+ a' <- f a n
98+ loop a' (n+1)
99+{-# INLINE foldM' #-}
src/Database/PostgreSQL/Simple/Internal/PQResultUtils.hsnew file+89
@@ -0,0 +1,89 @@
1+
2+------------------------------------------------------------------------------
3+-- |
4+-- Module: Database.PostgreSQL.Simple.Internal.PQResultUtils
5+-- Copyright: (c) 2011 MailRank, Inc.
6+-- (c) 2011-2012 Leon P Smith
7+-- License: BSD3
8+-- Maintainer: Leon P Smith <leon@melding-monads.com>
9+-- Stability: experimental
10+--
11+------------------------------------------------------------------------------
12+
13+
14+module Database.PostgreSQL.Simple.Internal.PQResultUtils
15+ ( finishQueryWith
16+ , getRowWith
17+ ) where
18+
19+import Control.Exception as E
20+import Data.ByteString (ByteString)
21+import Database.PostgreSQL.Simple.FromField (ResultError(..))
22+import Database.PostgreSQL.Simple.Ok
23+import Database.PostgreSQL.Simple.Types (Query(..))
24+import Database.PostgreSQL.Simple.Internal as Base
25+import Database.PostgreSQL.Simple.TypeInfo
26+import qualified Database.PostgreSQL.LibPQ as PQ
27+import qualified Data.ByteString.Char8 as B
28+import Control.Monad.Trans.Reader
29+import Control.Monad.Trans.State.Strict
30+
31+finishQueryWith :: RowParser r -> Connection -> Query -> PQ.Result -> IO [r]
32+finishQueryWith parser conn q result = do
33+ status <- PQ.resultStatus result
34+ case status of
35+ PQ.TuplesOk -> do
36+ nrows <- PQ.ntuples result
37+ ncols <- PQ.nfields result
38+ forM' 0 (nrows-1) $ \row ->
39+ getRowWith parser row ncols conn result
40+ PQ.EmptyQuery -> queryErr "query: Empty query"
41+ PQ.CommandOk -> queryErr "query resulted in a command response"
42+ PQ.CopyOut -> queryErr "query: COPY TO is not supported"
43+ PQ.CopyIn -> queryErr "query: COPY FROM is not supported"
44+ PQ.CopyBoth -> queryErr "query: COPY BOTH is not supported"
45+ PQ.SingleTuple -> queryErr "query: single-row mode is not supported"
46+ PQ.BadResponse -> throwResultError "query" result status
47+ PQ.NonfatalError -> throwResultError "query" result status
48+ PQ.FatalError -> throwResultError "query" result status
49+ where
50+ queryErr msg = throwIO $ QueryError msg q
51+
52+getRowWith :: RowParser r -> PQ.Row -> PQ.Column -> Connection -> PQ.Result -> IO r
53+getRowWith parser row ncols conn result = do
54+ let rw = Row row result
55+ let unCol (PQ.Col x) = fromIntegral x :: Int
56+ okvc <- runConversion (runStateT (runReaderT (unRP parser) rw) 0) conn
57+ case okvc of
58+ Ok (val,col) | col == ncols -> return val
59+ | otherwise -> do
60+ vals <- forM' 0 (ncols-1) $ \c -> do
61+ tinfo <- getTypeInfo conn =<< PQ.ftype result c
62+ v <- PQ.getvalue result row c
63+ return ( tinfo
64+ , fmap ellipsis v )
65+ throw (ConversionFailed
66+ (show (unCol ncols) ++ " values: " ++ show vals)
67+ Nothing
68+ ""
69+ (show (unCol col) ++ " slots in target type")
70+ "mismatch between number of columns to \
71+ \convert and number in target type")
72+ Errors [] -> throwIO $ ConversionFailed "" Nothing "" "" "unknown error"
73+ Errors [x] -> throwIO x
74+ Errors xs -> throwIO $ ManyErrors xs
75+
76+ellipsis :: ByteString -> ByteString
77+ellipsis bs
78+ | B.length bs > 15 = B.take 10 bs `B.append` "[...]"
79+ | otherwise = bs
80+
81+forM' :: (Ord n, Num n) => n -> n -> (n -> IO a) -> IO [a]
82+forM' lo hi m = loop hi []
83+ where
84+ loop !n !as
85+ | n < lo = return as
86+ | otherwise = do
87+ a <- m n
88+ loop (n-1) (a:as)
89+{-# INLINE forM' #-}