hpgsql 0.2.0.0 → 0.2.0.1
raw patch · 5 files changed
+171/−102 lines, 5 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Hpgsql.InternalTypes: HPgConnection :: !Socket -> !MVar Bool -> !MVar ByteString -> !MVar [(ByteString, STM ())] -> !Mutex -> !ConnectionString -> !AddrInfo -> !MVar EncodingContext -> !MVar (Map Text Text) -> !TVar InternalConnectionState -> !Int32 -> !Int32 -> !ConnectOpts -> HPgConnection
+ Hpgsql.InternalTypes: HPgConnection :: !Socket -> !MVar Bool -> !IORef ByteString -> !MVar [(ByteString, STM ())] -> !Mutex -> !ConnectionString -> !AddrInfo -> !MVar EncodingContext -> !MVar (Map Text Text) -> !TVar InternalConnectionState -> !Int32 -> !ByteString -> !ConnectOpts -> HPgConnection
- Hpgsql.InternalTypes: [cancelSecretKey] :: HPgConnection -> !Int32
+ Hpgsql.InternalTypes: [cancelSecretKey] :: HPgConnection -> !ByteString
- Hpgsql.InternalTypes: [recvBuffer] :: HPgConnection -> !MVar ByteString
+ Hpgsql.InternalTypes: [recvBuffer] :: HPgConnection -> !IORef ByteString
Files
- hpgsql.cabal +1/−1
- src/Hpgsql/Internal.hs +139/−92
- src/Hpgsql/InternalTypes.hs +3/−2
- src/Hpgsql/Msgs.hs +6/−6
- src/Hpgsql/SimpleParser.hs +22/−1
hpgsql.cabal view
@@ -1,7 +1,7 @@ cabal-version: 2.0 name: hpgsql-version: 0.2.0.0+version: 0.2.0.1 synopsis: Pure Haskell PostgreSQL driver (no libpq) description: hpgsql is a pure Haskell implementation of a PostgreSQL driver category: Database
src/Hpgsql/Internal.hs view
@@ -115,10 +115,12 @@ import Control.Exception.Safe (Exception (..), MonadThrow, SomeException, bracket, bracketOnError, finally, handleJust, mask, mask_, onException, throw, toException, tryJust) import Control.Monad (forM, forM_, join, unless, void, when) import Data.ByteString (ByteString)+import qualified Data.ByteString as BS import Data.ByteString.Internal (w2c) import qualified Data.ByteString.Lazy as LBS import Data.Data (Proxy (..)) import Data.Either (isLeft, isRight)+import Data.IORef (atomicWriteIORef, newIORef, readIORef) import Data.Int (Int32, Int64) import qualified Data.List as List import Data.List.NonEmpty (NonEmpty (..))@@ -152,7 +154,6 @@ import qualified Network.Socket.ByteString.Lazy as SocketLBS import Streaming (Of (..), Stream) import qualified Streaming as S-import qualified Streaming.Internal as SInternal import qualified Streaming.Prelude as S import System.IO (stderr) import System.IO.Error (isResourceVanishedError)@@ -245,7 +246,7 @@ False -> throwIrrecoverableError "Socket is not marked as non-blocking, which is not supported by hpgsql. You might be running on an unsupported platform" True -> pure () socketIsClosed <- newMVar False- recvBuffer <- newMVar mempty+ recvBuffer <- newIORef mempty sendBuffer <- newMVar mempty socketMutex <- mkMutex encodingContext <- newMVar (EncodingContext builtinPgTypesMap)@@ -261,7 +262,7 @@ preparedStatementNames = mempty, transactionStatusBeforeCurrentPipeline = TransIdle }- let hpgConnPartialDoNotReturn = HPgConnection sock socketIsClosed recvBuffer sendBuffer socketMutex originalConnStr addrInfo encodingContext connParams currentConnectionState 0 0 connOpts+ let hpgConnPartialDoNotReturn = HPgConnection sock socketIsClosed recvBuffer sendBuffer socketMutex originalConnStr addrInfo encodingContext connParams currentConnectionState 0 "" connOpts case connectOrCancel of CancelNotConnect cancelRequest _ -> do nonAtomicSendMsg hpgConnPartialDoNotReturn cancelRequest@@ -366,7 +367,7 @@ addrCanonName = Nothing } else do- addrInfos <- Socket.getAddrInfo (Just Socket.defaultHints) (Just $ Text.unpack hostname) (Just $ show port)+ addrInfos <- Socket.getAddrInfo (Just Socket.defaultHints {Socket.addrSocketType = Socket.Stream}) (Just $ Text.unpack hostname) (Just $ show port) case addrInfos of [] -> throwIrrecoverableError "Could not resolve address" addrInfo : _ -> pure addrInfo@@ -498,21 +499,31 @@ -- | Just like `receiveNextMsgWithMaskedContinuation` but passes a `Maybe a` to -- the continuation instead of throwing an exception on parser failure. On parsing -- failure, this makes sure the message buffer remains unaltered.-receiveNextMsgWithMaskedContinuation :: (Show a) => HPgConnection -> PgMsgParser a -> (a -> IO b) -> IO b+receiveNextMsgWithMaskedContinuation :: (Show a) => HPgConnection -> PgMsgParser a -> (a -> STM b) -> IO b receiveNextMsgWithMaskedContinuation conn parser f = receiveNextMsgWithMaskedContinuationButDontThrowOnParsingFailure conn parser $ \case Right p -> f p Left (msgIdentChar, mPgError) -> throw IrrecoverableHpgsqlError {hpgsqlDetails = "Could not parse postgres message with ident char " <> Text.pack (show msgIdentChar) <> ". This is an internal error in Hpgsql. Please report it.", innerException = toException <$> mPgError, relatedStatement = Nothing} +data ReceiveWhat a b where+ ReceiveDataRows :: ReceiveWhat DataRow [DataRow]+ ReceiveArbitraryMsg :: PgMsgParser a -> (Either (Char, Maybe PostgresError) a -> STM b) -> ReceiveWhat a b+ -- | Masks asynchronous exceptions in between the moment the message is extracted from -- the internal buffer and the supplied function runs to completion. -- This is important for control messages (i.e. not `DataRow`) because if you extract a -- message from the buffer, you must be able to update the connection's internal state, -- lest it will be left in a very broken place.--- CAREFUL: avoid doing networking or too much work in your supplied function. It must--- be really cheap!-receiveNextMsgWithMaskedContinuationButDontThrowOnParsingFailure :: (Show a) => HPgConnection -> PgMsgParser a -> (Either (Char, Maybe PostgresError) a -> IO b) -> IO b-receiveNextMsgWithMaskedContinuationButDontThrowOnParsingFailure conn@HPgConnection {socket, recvBuffer} parser f = do+receiveNextMsgWithMaskedContinuationButDontThrowOnParsingFailure :: (Show a) => HPgConnection -> PgMsgParser a -> (Either (Char, Maybe PostgresError) a -> STM b) -> IO b+receiveNextMsgWithMaskedContinuationButDontThrowOnParsingFailure conn parser f = receiveNextMsgGeneric conn (ReceiveArbitraryMsg parser f)++-- | Masks asynchronous exceptions in between the moment the message is extracted from+-- the internal buffer and the supplied function runs to completion.+-- This is important for control messages (i.e. not `DataRow`) because if you extract a+-- message from the buffer, you must be able to update the connection's internal state,+-- lest it will be left in a very broken place.+receiveNextMsgGeneric :: (Show a) => HPgConnection -> ReceiveWhat a b -> IO b+receiveNextMsgGeneric conn@HPgConnection {socket, recvBuffer} receiveWhat = do -- We need to preserve the invariant that the internal buffer's first byte is -- always the first byte of a valid Message while keeping this function -- interruptible.@@ -520,78 +531,99 @@ -- due to the presence of asynchronous exceptions. -- So we append to the buffer up until it has been fully fetched, -- and then extract it from the buffer in one piece.- currentBuf <- receiveUntilBufferHasAtLeast 5- let bufLen = LBS.length currentBuf- let charAndLength = LBS.take 5 currentBuf+ (initialBuf, initialBufLen) <- receiveUntilBufferHasAtLeast 5+ let charAndLength = LBS.take 5 initialBuf let (w2c -> msgIdentChar, lenbs) = fromMaybe (error "impossible") $ LBS.uncons charAndLength lenLeftToFetch :: Int64 = fromIntegral $ either error id (Cereal.decodeLazy @Int32 lenbs) - 4 fullMessageLen = 5 + lenLeftToFetch- restOfMsg <- LBS.drop 5 . LBS.take fullMessageLen <$> if bufLen >= fullMessageLen then pure currentBuf else receiveUntilBufferHasAtLeast fullMessageLen- receivedNoticeOrParameterSoTryAgain <- go msgIdentChar restOfMsg fullMessageLen+ (nowBuf, _nowBufLen) <- if initialBufLen >= fullMessageLen then pure (initialBuf, initialBufLen) else receiveUntilBufferHasAtLeast fullMessageLen+ let restOfMsg = LBS.drop 5 $ LBS.take fullMessageLen nowBuf+ receivedNoticeOrParameterSoTryAgain <- go msgIdentChar restOfMsg fullMessageLen nowBuf case receivedNoticeOrParameterSoTryAgain of- Nothing -> receiveNextMsgWithMaskedContinuationButDontThrowOnParsingFailure conn parser f+ Nothing -> receiveNextMsgGeneric conn receiveWhat Just res -> pure res where- -- We mask_ because if the supplied IO action runs with a message extracted from+ modifyIORefIO ioref io = do+ (!newC, !v) <- io+ atomicWriteIORef ioref newC+ pure v+ -- We mask_ because if the supplied STM action runs with a message extracted from -- the recvBuffer, then we _must_ remove that message from recvBuffer.- -- TODO: These IO actions are always STM transactions (some times just a `pure` call),- -- so maybe we should reflect that in the type to avoid the impression that- -- anything can happen.- go msgIdentChar restOfMsg fullMessageLen = mask_ $ modifyMVar recvBuffer $ \lbs -> do- let !bufferWithoutMsg =- if LBS.length lbs >= fullMessageLen- then LBS.drop fullMessageLen lbs- else- error "Bug in Hpgsql. Internal buffer's bytes weren't filled enough"+ -- Ideally we'd have non-retriable STM at the type-level here. Maybe later.+ -- Make sure to do very little work inside `go`!+ go msgIdentChar restOfMsg fullMessageLen nowBuf = mask_ $ modifyIORefIO recvBuffer $ do+ let bufferWithoutMsg = LBS.drop fullMessageLen nowBuf+ handleUnexpectedMsg onNotAnyReasonableMsg =+ -- This could be a Notification, NOTICE or a ParameterStatus message, since these+ -- can be received _at any time_ according to the docs.+ case parsePgMessage msgIdentChar restOfMsg (Left3 <$> msgParser @NotificationResponse <|> Middle3 <$> msgParser @NoticeResponse <|> Right3 <$> msgParser @ParameterStatus) of+ Just (Left3 notifResponse) -> do+ debugPrint "Received notification. Will add it to internal queue."+ STM.atomically $ do+ sttv <- STM.readTVar $ internalConnectionState conn+ STM.writeTQueue (notificationsReceived sttv) notifResponse+ pure (bufferWithoutMsg, Nothing)+ Just (Middle3 (NoticeResponse details)) -> do+ let severity =+ fromMaybe+ "Notice of unknown severity"+ (Map.lookup ErrorSeverity details)+ humanmsg = fromMaybe "no message" (Map.lookup ErrorHumanReadableMsg details)+ Text.hPutStrLn stderr $ decodeUtf8 $ LBS.toStrict $ severity <> ": " <> humanmsg+ pure (bufferWithoutMsg, Nothing)+ Just (Right3 (ParameterStatus {..})) -> do+ when (parameterName == "client_encoding" && parameterValue /= "UTF8") $ throwIrrecoverableError $ "Postgres sent us a change of client_encoding to not UTF8, and Hpgsql only supports UTF8. The encoding postgres sent us is " <> parameterValue+ modifyMVar_ (parameterStatusMap conn) $ \(!paramMap) -> pure (Map.insert parameterName parameterValue paramMap)+ pure (bufferWithoutMsg, Nothing)+ Nothing -> do+ -- Just in case this is a postgres error, it might include useful information,+ -- so we spit that out+ let mPgError = mkPostgresError "" <$> parsePgMessage msgIdentChar restOfMsg (msgParser @ErrorResponse)+ fmap (nowBuf,) $ Just <$> STM.atomically (onNotAnyReasonableMsg (msgIdentChar, mPgError))+ case receiveWhat of+ ReceiveDataRows ->+ -- Parse as many DataRows as we can to do as much work as we can per buffer "churn"+ case Parser.parseOnly (Parser.matchLeftUnconsumed (Parser.parseMany customDataRowParser)) (LBS.toStrict nowBuf) of+ Parser.ParseOk (unconsumedBuffer, msgs@(_ : _)) -> do+ debugPrint $ "Received " ++ show msgs+ pure (LBS.fromStrict unconsumedBuffer, Just msgs)+ _ -> handleUnexpectedMsg $ const $ pure [] -- No error when we stop receiving DataRows, only emptiness+ ReceiveArbitraryMsg parser f ->+ case parsePgMessage msgIdentChar restOfMsg parser of+ Just msg -> do+ debugPrint $ "Received " ++ show msg+ fmap (bufferWithoutMsg,) $ Just <$> STM.atomically (f (Right msg))+ Nothing -> handleUnexpectedMsg (f . Left) - case parsePgMessage msgIdentChar restOfMsg parser of- Just msg -> do- debugPrint $ "Received " ++ show msg- fmap (bufferWithoutMsg,) $ Just <$> f (Right msg)- Nothing -> do- -- This could be a Notification, NOTICE or a ParameterStatus message, since these- -- can be received _at any time_ according to the docs.- case parsePgMessage msgIdentChar restOfMsg (Left3 <$> msgParser @NotificationResponse <|> Middle3 <$> msgParser @NoticeResponse <|> Right3 <$> msgParser @ParameterStatus) of- Just (Left3 notifResponse) -> do- debugPrint "Received notification. Will add it to internal queue."- STM.atomically $ do- sttv <- STM.readTVar $ internalConnectionState conn- STM.writeTQueue (notificationsReceived sttv) notifResponse- pure (bufferWithoutMsg, Nothing)- Just (Middle3 (NoticeResponse details)) -> do- let severity =- fromMaybe- "Notice of unknown severity"- (Map.lookup ErrorSeverity details)- humanmsg = fromMaybe "no message" (Map.lookup ErrorHumanReadableMsg details)- Text.hPutStrLn stderr $ decodeUtf8 $ LBS.toStrict $ severity <> ": " <> humanmsg- pure (bufferWithoutMsg, Nothing)- Just (Right3 (ParameterStatus {..})) -> do- when (parameterName == "client_encoding" && parameterValue /= "UTF8") $ throwIrrecoverableError $ "Postgres sent us a change of client_encoding to not UTF8, and Hpgsql only supports UTF8. The encoding postgres sent us is " <> parameterValue- modifyMVar_ (parameterStatusMap conn) $ \(!paramMap) -> pure (Map.insert parameterName parameterValue paramMap)- pure (bufferWithoutMsg, Nothing)- Nothing ->- -- Just in case this is a postgres error, it might include useful information,- -- so we spit that out- let mPgError = mkPostgresError "" <$> parsePgMessage msgIdentChar restOfMsg (msgParser @ErrorResponse)- in fmap (lbs,) $ Just <$> f (Left (msgIdentChar, mPgError))+ -- Sadly we have to repeat the parsing of a DataRow message here, when it already+ -- exists in the FromPgMessage instance and in the body of this function. Maybe+ -- we can improve this later.+ customDataRowParser = do+ charAndLength <- Parser.take 5+ let (w2c -> msgIdentChar, lenbs) = fromMaybe (error "impossible") $ BS.uncons charAndLength+ lenLeftToFetch :: Int = fromIntegral $ either error id (Cereal.decode @Int32 lenbs) - 4+ if msgIdentChar == 'D'+ then do+ rowColumnData <- BS.drop 2 <$> Parser.take lenLeftToFetch+ pure $ DataRow rowColumnData+ else fail "Not a DataRow" -- \| Appends into the internal buffer by reading from the socket -- until the buffer has at least N bytes.- -- Returns the current buffer.- receiveUntilBufferHasAtLeast :: Int64 -> IO LBS.ByteString+ -- Returns the current buffer and its length.+ receiveUntilBufferHasAtLeast :: Int64 -> IO (LBS.ByteString, Int64) receiveUntilBufferHasAtLeast minBytesNecessary = do- currentBuffer <- readMVar recvBuffer+ currentBuffer <- readIORef recvBuffer let nBytesInBuffer = LBS.length currentBuffer if nBytesInBuffer >= minBytesNecessary- then pure currentBuffer+ then pure (currentBuffer, nBytesInBuffer) else do -- This takes from the kernel's recv buffer and appends to our buffer atomically, -- or an exception is thrown when receiving.- mask $ \restore -> rethrowAsIrrecoverable $ modifyMVar_ recvBuffer $ \lbs -> do+ mask $ \restore -> rethrowAsIrrecoverable $ do restore $ socketWaitRead socket someBytes <- timeDebugNonBlockingOperation "recv" $ recvNonBlocking socket (max 16000 $ fromIntegral $ minBytesNecessary - nBytesInBuffer)- pure (lbs <> LBS.fromStrict someBytes)+ atomicWriteIORef recvBuffer (currentBuffer <> LBS.fromStrict someBytes) receiveUntilBufferHasAtLeast minBytesNecessary sendCancellationRequest :: HPgConnection -> IO ()@@ -618,7 +650,7 @@ () -- Already cancelled, no need to send another Nothing -> internalConnectOrCancel- (CancelNotConnect (CancelRequest (connPid conn) (cancelSecretKey conn)) (connectedTo conn))+ (CancelNotConnect (CancelRequest conn.connPid conn.cancelSecretKey) conn.connectedTo) (connOpts conn) (originalConnStr conn) (secondsToDiffTime 30)@@ -725,26 +757,26 @@ -- We do have an assertion in `updateQueryStateIfFirstOrThrow` to ensure we aren't -- dismissive of messages arriving in unexpected order. receiveParseOrBindCompleteAtomically = receiveNextMsgWithMaskedContinuation conn (Right3 <$> msgParser @ParseComplete <|> Left3 <$> msgParser @ErrorResponse <|> Middle3 <$> msgParser @BindComplete) $ \msgE -> do- STM.atomically $ updateQueryStateIfFirstOrThrow $ case msgE of+ updateQueryStateIfFirstOrThrow $ case msgE of Right3 msg -> RespParseComplete msg Middle3 msg -> RespBindComplete msg Left3 err -> RespErrorResponse err receiveBindCompleteAtomically = receiveNextMsgWithMaskedContinuation conn (Right <$> msgParser @BindComplete <|> Left <$> msgParser @ErrorResponse) $ \msgE -> do- STM.atomically $ updateQueryStateIfFirstOrThrow $ case msgE of+ updateQueryStateIfFirstOrThrow $ case msgE of Right msg -> RespBindComplete msg Left err -> RespErrorResponse err receiveNoDataOrRowDescriptionOrCopyInResponseAtomically = receiveNextMsgWithMaskedContinuation conn (Right <$> (Right3 <$> msgParser @RowDescription <|> Left3 <$> msgParser @NoData <|> Middle3 <$> msgParser @CopyInResponse) <|> Left <$> msgParser @ErrorResponse) $ \msgE -> do- STM.atomically $ updateQueryStateIfFirstOrThrow $ case msgE of+ updateQueryStateIfFirstOrThrow $ case msgE of Right (Left3 msg) -> RespNoData msg Right (Middle3 msg) -> RespCopyInResponse msg Right (Right3 msg) -> RespRowDescription msg Left err -> RespErrorResponse err receiveDataRowOrCommandCompleteAtomically = receiveNextMsgWithMaskedContinuation conn (Right3 <$> msgParser @DataRow <|> Middle3 <$> msgParser @CommandComplete <|> Left3 <$> msgParser @ErrorResponse) $ \msgE -> do- STM.atomically $ updateQueryStateIfFirstOrThrow $ case msgE of+ updateQueryStateIfFirstOrThrow $ case msgE of Right3 msg -> RespDataRow msg Middle3 msg -> RespCommandComplete msg Left3 msg -> RespErrorResponse msg- receiveReadyForQueryAtomically = receiveNextMsgWithMaskedContinuation conn (msgParser @ReadyForQuery) $ \rq -> STM.atomically $ updateQueryStateIfFirstOrThrow $ RespReadyForQuery rq+ receiveReadyForQueryAtomically = receiveNextMsgWithMaskedContinuation conn (msgParser @ReadyForQuery) $ \rq -> updateQueryStateIfFirstOrThrow $ RespReadyForQuery rq getQueryStateIfFirstOrThrow :: STM QueryState getQueryStateIfFirstOrThrow = updateConnStateTxn conn $ \sttv -> do st <- STM.readTVar sttv@@ -865,33 +897,29 @@ pure (mERowDesc, pure $ Right cmd) (mERowDesc, Middle3 mDataRow) -> do let allOtherRows =- S.unfold- ( \() -> do- -- This is a bit ugly, but we try very carefully not to bear the cost of STM- -- transactions at all when receiving DataRows, since they can come in very large- -- amounts, but we need to make sure the _other_ important messages, like ErrorResponse- -- and CommandComplete, do update internal state.- mRow <- receiveNextMsgWithMaskedContinuationButDontThrowOnParsingFailure conn (msgParser @DataRow) pure- case mRow of- Right row -> pure $ Right (row :> ())- Left _ -> do- stateAfterNextMsg <- snd <$> receiveOutstandingResponseMsgsAtomically thisThreadId conn qryId- case stateAfterNextMsg of- ErrorResponseReceived _ err -> do- receiveReadyForQueryIfNecessary thisThreadId- pure $ Left $ Left err- CommandCompleteReceived _ cmd -> do- receiveReadyForQueryIfNecessary thisThreadId- pure $ Left $ Right cmd- ReadyForQueryReceived errOrCmd _ -> pure $ Left errOrCmd- _ -> throwIrrecoverableError "Internal error in Hpgsql. After a DataRow we should get either an ErrorResponse or a CommandComplete message"- )- ()+ S.concat $+ S.unfold+ ( \() -> do+ mRow <- receiveNextMsgGeneric conn ReceiveDataRows+ case mRow of+ rows@(_ : _) -> pure $ Right (rows :> ())+ [] -> do+ stateAfterNextMsg <- snd <$> receiveOutstandingResponseMsgsAtomically thisThreadId conn qryId+ case stateAfterNextMsg of+ ErrorResponseReceived _ err -> do+ receiveReadyForQueryIfNecessary thisThreadId+ pure $ Left $ Left err+ CommandCompleteReceived _ cmd -> do+ receiveReadyForQueryIfNecessary thisThreadId+ pure $ Left $ Right cmd+ ReadyForQueryReceived errOrCmd _ -> pure $ Left errOrCmd+ st -> throwIrrecoverableError $ "Internal error in Hpgsql. After the last DataRow we should get either an ErrorResponse or a CommandComplete message. State: " <> Text.pack (show st)+ )+ () finalStream = case mDataRow of Nothing -> allOtherRows Just dr ->- SInternal.Step $- dr :> allOtherRows+ dr `S.cons` allOtherRows pure (mERowDesc, finalStream) where receiveReadyForQueryIfNecessary :: WeakThreadId -> IO ()@@ -930,6 +958,9 @@ Right (CommandComplete n) -> pure n -- | Runs a query and streams results directly from the connection's socket, i.e. without using cursors.+-- Once you assign the returned Stream to a binding, you must consume that binding linearly, i.e.+-- you should never consume it more than once, for it _will_ give you wrong results if you do that.+-- This is normal behaviour for IO Streams, but it's worth emphasizing. -- -- Note on thread safety: it is important to note the same thread that runs this must -- be the thread that consumes the returned Stream, and the returned Stream must be@@ -939,6 +970,9 @@ queryS = querySWith rowDecoder -- | Runs a query and streams results directly from the connection's socket, i.e. without using cursors.+-- Once you assign the returned Stream to a binding, you must consume that binding linearly, i.e.+-- you should never consume it more than once, for it _will_ give you wrong results if you do that.+-- This is normal behaviour for IO Streams, but it's worth emphasizing. -- -- Note on thread safety: it is important to note the same thread that runs this must -- be the thread that consumes the returned Stream, and the returned Stream must be@@ -952,6 +986,10 @@ -- Prefer to use 'queryS' and 'querySWith', because 'RowDecoder' can typecheck PostgreSQL results -- even when no rows are returned by queries, and 'RowDecoderMonadic' cannot. --+-- Once you assign the returned Stream to a binding, you must consume that binding linearly, i.e.+-- you should never consume it more than once, for it _will_ give you wrong results if you do that.+-- This is normal behaviour for IO Streams, but it's worth emphasizing.+-- -- Note on thread safety: it is important to note the same thread that runs this must -- be the thread that consumes the returned Stream, and the returned Stream must be -- consumed completely (up to the last row or a postgres error) before you are able@@ -1202,10 +1240,16 @@ Just tid -> (`elem` [ThreadDied, ThreadFinished]) <$> threadStatus tid -- | Fetches any number of rows by streaming them directly from the socket.+-- Once you assign the returned Stream to a binding, you must consume that binding linearly, i.e.+-- you should never consume it more than once, for it _will_ give you wrong results if you do that.+-- This is normal behaviour for IO Streams, but it's worth emphasizing. pipelineS :: (FromPgRow a) => Query -> Pipeline (IO (Stream (Of a) IO ())) pipelineS = pipelineSWith rowDecoder -- | Fetches any number of rows by streaming them directly from the socket, with a custom row decoder.+-- Once you assign the returned Stream to a binding, you must consume that binding linearly, i.e.+-- you should never consume it more than once, for it _will_ give you wrong results if you do that.+-- This is normal behaviour for IO Streams, but it's worth emphasizing. pipelineSWith :: RowDecoder a -> Query -> Pipeline (IO (Stream (Of a) IO ())) pipelineSWith rowparser@(RowDecoder _ _ expectedColFmts) (lastAndInitNE . breakQueryIntoStatements -> (firstQueriesToSend, lastQueryToSend)) = Pipeline@@ -1219,6 +1263,9 @@ -- | Prefer to use 'pipelineS' and 'pipelineSWith', because 'RowDecoder' can typecheck PostgreSQL results -- even when no rows are returned by queries, and 'RowDecoderMonadic' cannot.+-- Once you assign the returned Stream to a binding, you must consume that binding linearly, i.e.+-- you should never consume it more than once, for it _will_ give you wrong results if you do that.+-- This is normal behaviour for IO Streams, but it's worth emphasizing. pipelineSMWith :: RowDecoderMonadic a -> Query -> Pipeline (IO (Stream (Of a) IO ())) pipelineSMWith rowparser (lastAndInitNE . breakQueryIntoStatements -> (firstQueriesToSend, lastQueryToSend)) = Pipeline
src/Hpgsql/InternalTypes.hs view
@@ -72,6 +72,7 @@ #endif import qualified Control.Concurrent.STM as STM import Data.Hashable (hash)+import Data.IORef (IORef) import Data.Set (Set) import Hpgsql.Base (lastTwoAndInit, maximumOnOrDef, minimumOnOrDef) import Hpgsql.Builder (BinaryField)@@ -456,7 +457,7 @@ data HPgConnection = HPgConnection { socket :: !Socket, socketClosed :: !(MVar Bool),- recvBuffer :: !(MVar LBS.ByteString),+ recvBuffer :: !(IORef LBS.ByteString), sendBuffer :: !(MVar [(LBS.ByteString, STM ())]), socketMutex :: !Mutex, originalConnStr :: !ConnectionString,@@ -465,7 +466,7 @@ parameterStatusMap :: !(MVar (Map Text Text)), internalConnectionState :: !(TVar InternalConnectionState), connPid :: !Int32,- cancelSecretKey :: !Int32,+ cancelSecretKey :: !ByteString, connOpts :: !ConnectOpts }
src/Hpgsql/Msgs.hs view
@@ -87,14 +87,14 @@ PasswordMessage AuthenticationMethod String String deriving stock (Show) -data BackendKeyData = BackendKeyData {backendPid :: Int32, backendSecretKey :: Int32}+data BackendKeyData = BackendKeyData {backendPid :: !Int32, backendSecretKey :: !BS.ByteString} deriving stock (Show) data Bind = Bind {paramsValuesInOrder :: ![BinaryField], resultColumnFmts :: !Int, preparedStmtHash :: !(Maybe String)} deriving stock (Show) -- | PId first, secret key second-data CancelRequest = CancelRequest Int32 Int32+data CancelRequest = CancelRequest Int32 ByteString deriving stock (Show) newtype CopyData = CopyData Builder@@ -160,9 +160,9 @@ _ -> Nothing instance FromPgMessage BackendKeyData where- msgParser = PgMsgParser $ \c (LBS.splitAt 4 -> (pidBS, secretBS)) -> case c of- 'K' -> case (,) <$> Cereal.decodeLazy @Int32 pidBS <*> Cereal.decodeLazy @Int32 secretBS of- Right (pid, secret) -> Just $ BackendKeyData {backendPid = pid, backendSecretKey = secret}+ msgParser = PgMsgParser $ \c (LBS.splitAt 4 -> (pidBS, backendSecretKey)) -> case c of+ 'K' -> case Cereal.decodeLazy @Int32 pidBS of+ Right pid -> Just $ BackendKeyData {backendPid = pid, backendSecretKey = LBS.toStrict backendSecretKey} Left _ -> Nothing _ -> Nothing @@ -191,7 +191,7 @@ instance ToPgMessage CancelRequest where toPgMessage (CancelRequest pid secret) =- Builder.int32BE (4 + 4 + 4 + 4) <> Builder.int32BE 80877102 <> Builder.int32BE pid <> Builder.int32BE secret+ Builder.int32BE (fromIntegral $ 4 + 4 + 4 + BS.length secret) <> Builder.int32BE 80877102 <> Builder.int32BE pid <> Builder.byteString secret instance ToPgMessage CopyData where toPgMessage (CopyData bs) =
src/Hpgsql/SimpleParser.hs view
@@ -12,6 +12,8 @@ take, endOfInput, match,+ parseMany,+ matchLeftUnconsumed, ) where @@ -32,7 +34,7 @@ (String -> r) -> -- \^ failure continuation (a -> ByteString -> r) ->- -- \^ success continuation+ -- \^ success continuation, taking left-unparsed ByteString and parsed value r } @@ -85,6 +87,14 @@ ks mempty bs {-# INLINE take #-} +parseMany :: Parser a -> Parser [a]+parseMany p = Parser $ \bs' _kf ks -> let (vs, rest) = go bs' in ks vs rest+ where+ go bs = case parseOnly (matchLeftUnconsumed p) bs of+ ParseOk (unconsumed, v) -> let (vs, rest) = go unconsumed in (v : vs, rest)+ ParseFail _ -> ([], bs)+{-# INLINE parseMany #-}+ -- | Succeeds only when the input has been fully consumed. endOfInput :: Parser () endOfInput = Parser $ \bs kf ks ->@@ -104,3 +114,14 @@ in ks (consumed, a) bs' ) {-# INLINE match #-}++-- | Run a parser and additionally return the unconsumed/unparsed ByteString.+matchLeftUnconsumed :: Parser a -> Parser (ByteString, a)+matchLeftUnconsumed (Parser p) = Parser $ \bs kf ks ->+ p+ bs+ kf+ ( \a bs' ->+ ks (bs', a) bs'+ )+{-# INLINE matchLeftUnconsumed #-}