pqi-native-0.0.1.0: src/transport/Pqi/Native/Transport/Message.hs
-- | The PostgreSQL frontend\/backend wire messages: builders ('Poker.Write') for
-- the frontend messages we send, and a decoder for the backend messages we
-- receive. Message framing (@[type byte][Int32 length][body]@, with the
-- startup message lacking a type byte) lives here too.
module Pqi.Native.Transport.Message
( -- * Frontend messages
startupMessage,
sslRequest,
cancelRequest,
passwordMessage,
saslInitialResponse,
saslResponse,
queryMessage,
parseMessage,
bindMessage,
describeStatementMessage,
describePortalMessage,
executeMessage,
closeStatementMessage,
closePortalMessage,
syncMessage,
flushMessage,
terminateMessage,
copyDataMessage,
copyDoneMessage,
copyFailMessage,
-- * Backend messages
BackendMessage (..),
FieldDescription (..),
decodeBackendMessage,
)
where
import qualified Data.ByteString as ByteString
import Pqi.Native.Comms
import Pqi.Native.Transport.Prelude
import qualified PtrPoker.Write as Poker
-- * Framing helpers
-- | Frame a typed frontend message: type byte, then a length covering the
-- length field and the body, then the body.
framed :: Word8 -> Poker.Write -> Poker.Write
framed typeByte body =
Poker.word8 typeByte
<> Poker.bInt32 (fromIntegral (4 + Poker.writeSize body))
<> body
-- | A null-terminated string.
cstr :: ByteString -> Poker.Write
cstr value = Poker.byteString value <> Poker.word8 0
formatCodes :: [Int16] -> Poker.Write
formatCodes codes =
Poker.bInt16 (fromIntegral (length codes)) <> foldMap Poker.bInt16 codes
-- * Frontend messages
-- | The startup message (no type byte), requesting protocol 3.0 with the given
-- parameters (e.g. @user@, @database@).
startupMessage :: [(ByteString, ByteString)] -> Poker.Write
startupMessage parameters =
let body =
Poker.bInt32 196608 -- protocol version 3.0 == (3 << 16)
<> foldMap (\(k, v) -> cstr k <> cstr v) parameters
<> Poker.word8 0
in Poker.bInt32 (fromIntegral (4 + Poker.writeSize body)) <> body
-- | The SSL negotiation request (no type byte).
sslRequest :: Poker.Write
sslRequest = Poker.bInt32 8 <> Poker.bInt32 80877103
-- | A cancel request (no type byte), sent on a fresh connection: the cancel
-- request code followed by the target backend's process ID and secret key.
cancelRequest :: Int32 -> Int32 -> Poker.Write
cancelRequest processId secretKey =
Poker.bInt32 16 <> Poker.bInt32 80877102 <> Poker.bInt32 processId <> Poker.bInt32 secretKey
-- | A cleartext or MD5 password response (message type @p@).
passwordMessage :: ByteString -> Poker.Write
passwordMessage password = framed 0x70 (cstr password)
-- | A SASL initial response (message type @p@): the chosen mechanism and the
-- client-first message.
saslInitialResponse :: ByteString -> ByteString -> Poker.Write
saslInitialResponse mechanism initialData =
framed 0x70
$ cstr mechanism
<> Poker.bInt32 (fromIntegral (ByteString.length initialData))
<> Poker.byteString initialData
-- | A subsequent SASL response (message type @p@).
saslResponse :: ByteString -> Poker.Write
saslResponse responseData = framed 0x70 (Poker.byteString responseData)
-- | A simple @Query@ message.
queryMessage :: ByteString -> Poker.Write
queryMessage sql = framed 0x51 (cstr sql)
-- | A @Parse@ message: statement name, SQL, and parameter type OIDs.
parseMessage :: ByteString -> ByteString -> [Word32] -> Poker.Write
parseMessage name sql oids =
framed 0x50
$ cstr name
<> cstr sql
<> Poker.bInt16 (fromIntegral (length oids))
<> foldMap Poker.bWord32 oids
-- | A @Bind@ message: portal, statement, per-parameter format codes, parameter
-- values ('Nothing' for SQL @NULL@), and result format codes.
bindMessage :: ByteString -> ByteString -> [Int16] -> [Maybe ByteString] -> [Int16] -> Poker.Write
bindMessage portal statement parameterFormats values resultFormats =
framed 0x42
$ cstr portal
<> cstr statement
<> formatCodes parameterFormats
<> Poker.bInt16 (fromIntegral (length values))
<> foldMap bindValue values
<> formatCodes resultFormats
where
bindValue = \case
Nothing -> Poker.bInt32 (-1)
Just value -> Poker.bInt32 (fromIntegral (ByteString.length value)) <> Poker.byteString value
-- | A @Describe@ message targeting a prepared statement.
describeStatementMessage :: ByteString -> Poker.Write
describeStatementMessage name = framed 0x44 (Poker.word8 0x53 <> cstr name)
-- | A @Describe@ message targeting a portal.
describePortalMessage :: ByteString -> Poker.Write
describePortalMessage name = framed 0x44 (Poker.word8 0x50 <> cstr name)
-- | An @Execute@ message: portal name and a maximum row count (0 = unlimited).
executeMessage :: ByteString -> Int32 -> Poker.Write
executeMessage portal maxRows = framed 0x45 (cstr portal <> Poker.bInt32 maxRows)
-- | A @Close@ message targeting a prepared statement.
closeStatementMessage :: ByteString -> Poker.Write
closeStatementMessage name = framed 0x43 (Poker.word8 0x53 <> cstr name)
-- | A @Close@ message targeting a portal.
closePortalMessage :: ByteString -> Poker.Write
closePortalMessage name = framed 0x43 (Poker.word8 0x50 <> cstr name)
-- | A @Sync@ message.
syncMessage :: Poker.Write
syncMessage = framed 0x53 mempty
-- | A @Flush@ message.
flushMessage :: Poker.Write
flushMessage = framed 0x48 mempty
-- | A @Terminate@ message.
terminateMessage :: Poker.Write
terminateMessage = framed 0x58 mempty
-- | A @CopyData@ message.
copyDataMessage :: ByteString -> Poker.Write
copyDataMessage payload = framed 0x64 (Poker.byteString payload)
-- | A @CopyDone@ message.
copyDoneMessage :: Poker.Write
copyDoneMessage = framed 0x63 mempty
-- | A @CopyFail@ message with an error string.
copyFailMessage :: ByteString -> Poker.Write
copyFailMessage reason = framed 0x66 (cstr reason)
-- * Backend messages
-- | The description of one result column, from a @RowDescription@ message.
data FieldDescription = FieldDescription
{ name :: ByteString,
tableOid :: Word32,
columnAttributeNumber :: Int16,
typeOid :: Word32,
typeSize :: Int16,
typeModifier :: Int32,
formatCode :: Int16
}
deriving stock (Eq, Show)
-- | A decoded backend message. Only the variants the adapter acts on are
-- distinguished; anything else becomes 'UnknownMessage'.
data BackendMessage
= AuthenticationOk
| AuthenticationCleartextPassword
| AuthenticationMD5Password ByteString
| AuthenticationSASL [ByteString]
| AuthenticationSASLContinue ByteString
| AuthenticationSASLFinal ByteString
| ParameterStatus ByteString ByteString
| BackendKeyData Int32 Int32
| ReadyForQuery Word8
| RowDescription [FieldDescription]
| DataRow [Maybe ByteString]
| CommandComplete ByteString
| EmptyQueryResponse
| ErrorResponse [(Word8, ByteString)]
| NoticeResponse [(Word8, ByteString)]
| ParseComplete
| BindComplete
| CloseComplete
| ParameterDescription [Word32]
| NoData
| PortalSuspended
| NotificationResponse Int32 ByteString ByteString
| CopyInResponse Int16 [Int16]
| CopyOutResponse Int16 [Int16]
| CopyData ByteString
| CopyDone
| FunctionCallResponse (Maybe ByteString)
| UnknownMessage Word8 ByteString
deriving stock (Eq, Show)
-- | Decode a backend message from its type byte and body.
decodeBackendMessage :: Word8 -> ByteString -> Either DecodingError BackendMessage
decodeBackendMessage typeByte body =
case typeByte of
0x52 -> runDecoder authentication body
0x53 -> runDecoder (ParameterStatus <$> cstring <*> cstring) body
0x4b -> runDecoder (BackendKeyData <$> int32 <*> int32) body
0x5a -> runDecoder (ReadyForQuery <$> word8) body
0x54 -> runDecoder (RowDescription <$> repeatedInt16 fieldDescription) body
0x44 -> runDecoder (DataRow <$> repeatedInt16 columnValue) body
0x43 -> runDecoder (CommandComplete <$> cstring) body
0x49 -> Right EmptyQueryResponse
0x45 -> runDecoder (ErrorResponse <$> noticeFields) body
0x4e -> runDecoder (NoticeResponse <$> noticeFields) body
0x31 -> Right ParseComplete
0x32 -> Right BindComplete
0x33 -> Right CloseComplete
0x74 -> runDecoder (ParameterDescription <$> repeatedInt16 word32) body
0x6e -> Right NoData
0x73 -> Right PortalSuspended
0x41 -> runDecoder (NotificationResponse <$> int32 <*> cstring <*> cstring) body
0x47 -> runDecoder (uncurry CopyInResponse <$> copyResponse) body
0x48 -> runDecoder (uncurry CopyOutResponse <$> copyResponse) body
0x64 -> runDecoder (CopyData <$> remaining) body
0x63 -> Right CopyDone
0x56 -> runDecoder (FunctionCallResponse <$> columnValue) body
other -> Right (UnknownMessage other body)
-- | Repeat a decoder an @Int16@-prefixed number of times.
repeatedInt16 :: Decoder a -> Decoder [a]
repeatedInt16 element = do
count <- int16
replicateM (fromIntegral count) element
fieldDescription :: Decoder FieldDescription
fieldDescription =
FieldDescription
<$> cstring
<*> word32
<*> int16
<*> word32
<*> int16
<*> int32
<*> int16
columnValue :: Decoder (Maybe ByteString)
columnValue = do
len <- int32
if len < 0
then pure Nothing
else Just <$> bytes (fromIntegral len)
-- | The fields of an @ErrorResponse@\/@NoticeResponse@: @(code, value)@ pairs,
-- terminated by a zero code byte.
noticeFields :: Decoder [(Word8, ByteString)]
noticeFields = do
code <- word8
if code == 0
then pure []
else do
value <- cstring
((code, value) :) <$> noticeFields
copyResponse :: Decoder (Int16, [Int16])
copyResponse = do
overall <- fromIntegral <$> word8
columns <- repeatedInt16 int16
pure (overall, columns)
authentication :: Decoder BackendMessage
authentication = do
subtype <- int32
case subtype of
0 -> pure AuthenticationOk
3 -> pure AuthenticationCleartextPassword
5 -> AuthenticationMD5Password <$> bytes 4
10 -> AuthenticationSASL <$> saslMechanisms
11 -> AuthenticationSASLContinue <$> remaining
12 -> AuthenticationSASLFinal <$> remaining
other -> throwE (UnexpectedAuthType other)
where
saslMechanisms = do
mechanism <- cstring
if ByteString.null mechanism
then pure []
else (mechanism :) <$> saslMechanisms