| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
Hmssql.Tds.Parser
Description
Reading the bytes of a TDS response, when those bytes arrive in pieces.
The problem
A server's response is one long sequence of bytes, but it reaches the client as a series of packets, and a packet can end anywhere: between two columns, in the middle of an integer, or thousands of bytes into a long value. A reader that reaches the end of a packet has to stop, wait for the next one, and carry on exactly where it was. Most of the code that understands TDS shouldn't have to know that any of this happens.
Recipes
A recipe describes what to read and in what order, written as ordinary monadic code. This one reads a length byte and then that many bytes:
shortValue :: Parser B.ByteString shortValue = word8 >>= \n -> boundedBytes (fromIntegral n)
A recipe is only a value, so defining one reads nothing. It says nothing about
packets either, so it gives the same answer wherever the packets happen to end.
parseChunks runs a recipe over payloads already in memory, and returns the
result and the input left over:
parseChunks shortValue ["\3abcdef"] -- ("abc", ["def"])
parseChunks shortValue ["\3a", "b", "cdef"] -- ("abc", ["def"])Suspending
When a recipe runs out of input partway through, it doesn't fail and it doesn't block. It returns a suspended computation, which holds everything it still has to do, including what it has already read. Whoever is running the recipe, called its driver, fetches the next payload and gives it to that computation:
case runParser shortValue "\3a" of
Right (Got value rest) -> ... -- complete within this payload
Left suspended -> inspect suspended >>= \case
Right (NeedMore continue) -> ... -- call continue (More nextPayload)
...This is what keeps the recipes simple. Where a read has got to is simply where
the code is, so a recipe holds no record of its column or its position. It is
also what keeps the module independent of the transport, because TLS, SMP
sessions and the packet framing all sit behind the driver. Most callers should
use runFrom or parseChunks rather than write a driver, because a driver has a
rule to follow, which runFrom describes.
Writing your own
A recipe for something hmssql doesn't read yet needs only the reads below and
do notation. fail stops with a ProtocolError. For example, a token with a
16-bit length around a list of strings:
names :: Parser [Text]
names = do
size <- word16le
isolate (fromIntegral size) $ do
n <- word8
count (fromIntegral n) bVarCharchunk is the most general read, because it gives whatever part of the requested
bytes is available, so any format can be built from it.
A recipe can fail in two ways, which mean different things. Throwing, as fail
does, ends the recipe, so nothing more can be read and the connection is broken.
Use it for bytes that make no sense. A value that was read correctly but can't be
used is a failValue instead, once its bytes have been read. It skips the rest of
the recipe up to the nearest recover or attempt, which goes on reading from where
the failure was.
This module only reads. To send something hmssql has no type for, such as a
table-valued parameter, build a Request whose body uses
Hmssql.Tds.PacketWriter and the builders of Hmssql.Tds.Client.
Synopsis
- type Parser = Recipe ()
- parse :: Parser a -> ByteString -> Stream Await IO (Got a)
- runParser :: Parser a -> ByteString -> Either (Stream Await IO (Got a)) (Got a)
- runParserAt :: Parser a -> ByteString -> Int -> Either (Stream Await IO (Got (Either ReadFailure a))) (At a)
- parseChunks :: Parser a -> [ByteString] -> IO (a, [ByteString])
- runFrom :: IO Feed -> Parser a -> ByteString -> IO (Got a)
- data Got a = Got !a !ByteString
- data At a
- = At !a !Int
- | FailedAt !ReadFailure !Int
- data Await next
- data Feed
- feedBytes :: Feed -> Maybe ByteString
- newtype Runner = Runner {}
- feedRunner :: IO Feed -> Runner
- effect :: IO a -> Parser a
- consume :: Stream (Of ByteString) Parser () -> (Stream (Of ByteString) IO () -> IO a) -> Parser (Maybe a)
- consumeWhole :: ByteString -> (Stream (Of ByteString) IO () -> IO a) -> Parser (Maybe a)
- dynamic :: Recipe env a -> Recipe env a
- oneShotRecipe :: Recipe env a -> Recipe env a
- failValue :: ReadFailure -> Recipe env a
- recover :: Recipe env a -> (ReadFailure -> Recipe env a) -> Recipe env a
- attempt :: Recipe env a -> Recipe env (Either ReadFailure a)
- data Recipe env a
- ask :: Recipe env env
- withEnv :: env -> Recipe env a -> Parser a
- liftParser :: Parser a -> Recipe env a
- peekToken :: Parser (Maybe Word8)
- peekWord8 :: Parser Word8
- word8 :: Parser Word8
- word16le :: Parser Word16
- word32le :: Parser Word32
- word64le :: Parser Word64
- word32be :: Parser Word32
- bytes :: Int -> Int -> Parser ByteString
- ownedBytes :: Int -> Int -> Parser ByteString
- boundedBytes :: Int -> Parser ByteString
- ownedBoundedBytes :: Int -> Parser ByteString
- chunk :: Int -> Parser ByteString
- chunkOrDiscard :: Int -> Parser (Maybe ByteString)
- skip :: Int -> Parser ()
- count :: Int -> Parser a -> Parser [a]
- bVarChar :: Parser Text
- usVarChar :: Parser Text
- isolate :: Int -> Parser a -> Parser a
- limited :: Int -> Parser a -> Parser a
- data ValueBytes
- valueBytes :: ValueBytes -> ByteString
- keepBytes :: ValueBytes -> ByteString
- bytesValue :: Int -> Int -> Parser (Maybe ValueBytes)
- convertBytes :: Int -> Int -> (ByteString -> Either ReadFailure a) -> (ByteString -> Either ReadFailure a) -> Parser a
- prefixedValue :: Int -> Int -> (ByteString -> Either ReadFailure a) -> Recipe env a -> Recipe env a -> Recipe env a
- prefixedRowValue :: Int -> Int -> Int -> (ByteString -> Either ReadFailure a) -> Recipe ByteString a -> Recipe ByteString a -> Recipe ByteString a
- data Accumulator
- accumulator :: Maybe Word64 -> Accumulator
- append :: Int -> Accumulator -> ByteString -> IO Accumulator
- finish :: Accumulator -> IO ByteString
- data ReadFailure
- data DecodeError = DecodeError {
- decodeErrorColumn :: !(Maybe Text)
- decodeErrorMessage :: !String
- newtype ProtocolError = ProtocolError String
- data ResultAbandoned = ResultAbandoned
Recipes and their driver
parse :: Parser a -> ByteString -> Stream Await IO (Got a) Source #
Start a recipe on a payload, as a suspended computation for a driver to run with
inspect. Pass an empty payload to start with no input. A failed value that the
recipe didn't recover from is a ProtocolError.
runParserAt :: Parser a -> ByteString -> Int -> Either (Stream Await IO (Got (Either ReadFailure a))) (At a) Source #
runParser from a position in the payload, for a driver that reads one
recipe after another from the same payload, as the connection reads rows, and
handles a failed value itself. A recipe that finishes within the payload gives
where it got to as a position in it, so the driver keeps the payload and a
position rather than building the unread bytes after each recipe. A failed
value that nothing recovered is FailedAt rather than a ProtocolError; a row's
recipe (CompiledRow) leaves its failure for the driver this
way. A recipe that suspends gives its failure as a Left at the end.
parseChunks :: Parser a -> [ByteString] -> IO (a, [ByteString]) Source #
Run a recipe over payloads already in memory, followed by the end of the message. The result is the recipe's value and whatever input it didn't read.
runFrom :: IO Feed -> Parser a -> ByteString -> IO (Got a) Source #
Run a recipe to its end, starting with this payload and fetching the rest from the
action. It also runs any consumers the recipe holds, with feedRunner for their
pulls. The result includes the unread part of the last payload.
This is the driver to use unless you need something it doesn't do. A driver of your
own must follow one rule: it must run each step of a suspended recipe only once.
After inspect, save what inspect returned, and not the computation it was given,
before anything can interrupt the driver. Running a step twice repeats its effects,
and an Accumulator it holds would write into memory it may already have given away.
A recipe's result, and the part of the current payload it didn't read. The next recipe starts with those bytes.
Constructors
| Got !a !ByteString |
Where a recipe that finished within its payload got to (runParserAt): its
value, or the failed value (failValue) that nothing recovered, and in either
case the position after it in the same payload.
Constructors
| At !a !Int | |
| FailedAt !ReadFailure !Int |
What a suspended recipe asks its driver for. A suspended recipe is a Stream of
these requests, so a driver sees three things as it runs one. Step is one of these
requests, Effect is a short piece of IO that the driver just runs, such as growing
a value's buffer, and Return is the final Got. Using inspect from the
streaming package runs the effects and gives the next request or the result.
Constructors
| NeedMore (Feed -> next) | The recipe needs more input. The driver fetches a payload and calls this function, which gives the rest of the recipe. |
| RunConsumer !Bool (Runner -> IO x) (Maybe x -> next) | The recipe has reached a value that a caller's code wants to read as it
arrives. The driver runs that code with a |
What a driver gives a suspended recipe: the next payload, with the transport and the TDS packet headers already removed, or the end of the message.
Constructors
| More !ByteString | The next payload, to read as usual. |
| Discard !ByteString | The next payload of a response being thrown away. Nobody will use a value
that is still being assembled, so its recipe may release its buffer and skip the
rest of the value. See |
| EndOfMessage | There are no more payloads. This is the normal way to finish at a token
boundary ( |
How a streaming consumer gets the next piece of its value. consume gives the
consumer a stream, and each pull from that stream calls the Runner.
The reference holds the suspended recipe that reads the value. runAwait runs
that recipe until it produces its next result, fetching payloads whenever it needs
them, and it saves its progress back in the reference as it goes, so that an
interrupted pull doesn't repeat anything.
The driver supplies the Runner because only the driver knows where the payloads
come from and what a pull must hold while it reads them. The connection's own
Runner masks each pull and reads from that response's packets. feedRunner builds
one from a function that returns the next payload.
feedRunner :: IO Feed -> Runner Source #
A Runner that gets payloads from this action. Its caller decides what to mask
and what to do about exceptions. It saves its progress before each step, so that
retrying after a failed fetch, or looking again at a finished value, repeats nothing.
A value's recipe that asks to run a consumer of its own is a ProtocolError, and it
stays failed in the reference, so that later cleanup can't run the consumer.
consume :: Stream (Of ByteString) Parser () -> (Stream (Of ByteString) IO () -> IO a) -> Parser (Maybe a) Source #
Give a stream of bytes to a function that reads it as the bytes arrive, then carry
on with the recipe. This is how a caller's code reads a large value without it being
in memory (streamField is built on it). The result is what the
function returned, or Nothing when the driver is throwing the response away and
didn't run it. Whatever part of the stream the function didn't read is read past
afterwards, so the recipe continues in the right place either way. An exception
the function throws ends the driver instead (RunConsumer); the saved step then
still holds the value, and a drain that resumes it reads past the rest.
The driver runs the function (RunConsumer), and each pull goes through the driver's
Runner. Each chunk is a copy, so it can be kept. Pulling the same part of the
stream twice, or pulling after the function has returned, throws ResultAbandoned.
consumeWhole :: ByteString -> (Stream (Of ByteString) IO () -> IO a) -> Parser (Maybe a) Source #
consume for a value already read whole: the function gets a stream of one
chunk, these bytes (none, for an empty value), which it may keep, and the recipe goes on after it with no
reading of its own. The driver runs the function as it runs consume's, and the
stream has the same rules, but the function never pulls from the response, so a
driver can run it among the rows it has already read. This is what
streamField does with a value small enough to read whole first,
where consume's references, masked pulls and drain per value cost several
times what the value does.
dynamic :: Recipe env a -> Recipe env a Source #
A recipe chosen at run time, such as a column's prepared recipe, which GHC therefore calls without seeing into it. Wrapping it here costs nothing in the recipe and saves the 16 bytes that the call would otherwise allocate for its position. Leave it off a recipe GHC can inline, because there the wrapper only adds a table lookup.
oneShotRecipe :: Recipe env a -> Recipe env a Source #
The same recipe, for one that is built to be run once, such as a step of a loop that builds the rest of a row's recipe as it reads. GHC then takes the recipe's input along with the function's own arguments, rather than build the recipe as closures and thunks and run it after. On a recipe that runs more than once, such as a prepared one, the work before its input would be repeated for every run.
Failed values
A value that was read but can't be used fails its recipe with failValue,
and recover or attempt catches the failure further out, where the reading
can go on. A row decoder uses this so that a failed cell stops the row without
every cell's result being an Either.
failValue :: ReadFailure -> Recipe env a Source #
Fail with a value that was read but can't be used. The bytes after it are left
for whatever recover runs next.
recover :: Recipe env a -> (ReadFailure -> Recipe env a) -> Recipe env a Source #
Run a recipe, and if a value in it fails (failValue), run the handler from
where the failure left the input.
attempt :: Recipe env a -> Recipe env (Either ReadFailure a) Source #
Run a recipe, and give its failed value, if it has one, as a Left.
Read-only environments
A recipe can read a value that isn't in the input. A row decoder uses this for the row's null bitmap, so that one recipe, prepared once, serves every row:
plusBase :: Recipe Word8 Word8 plusBase = (+) <$> ask <*> liftParser word8 parseChunks (withEnv 10 plusBase) ["\5"] -- (15, [])
A recipe that can also read a value of type env with ask, which withEnv
supplies when the recipe runs. It reads input and suspends just as a Parser does.
withEnv :: env -> Recipe env a -> Parser a Source #
Supply the environment, which makes the recipe a Parser. The same recipe can be
used again with another value.
liftParser :: Parser a -> Recipe env a Source #
Primitive reads
peekToken :: Parser (Maybe Word8) Source #
The next byte, which is left unread, or Nothing if the message has ended. This
is how to look for the next token, because it is the one read for which the end of
the message isn't an error.
peekWord8 :: Parser Word8 Source #
The next byte, which is left unread. The end of the message is a ProtocolError.
bytes :: Int -> Int -> Parser ByteString Source #
Exactly this many bytes (the second argument), in one ByteString. If they lie
within the current payload, the result is a slice of it and nothing is copied.
Otherwise they are copied into a buffer of their own as they arrive.
The first argument is how much may be allocated before the bytes have arrived, as
append describes. It protects against a server that declares a huge length.
A slice keeps its whole packet alive for as long as it is held, so use ownedBytes
for anything kept after the current token.
ownedBytes :: Int -> Int -> Parser ByteString Source #
Like bytes, but the result is always cheap to keep, because a slice is copied.
boundedBytes :: Int -> Parser ByteString Source #
bytes that may allocate the whole length ahead. This is for short metadata whose
length field is small enough to bound what a server can ask for, such as the length
byte of a B_VARCHAR.
ownedBoundedBytes :: Int -> Parser ByteString Source #
ownedBytes that may allocate the whole length ahead, as boundedBytes may.
chunk :: Int -> Parser ByteString Source #
Between 1 and this many bytes, as a slice of the current payload, waiting for a payload if there are none left. Asking for zero gives an empty result without needing input. Loop to read more than one payload holds, and copy what you keep.
chunkOrDiscard :: Int -> Parser (Maybe ByteString) Source #
skip :: Int -> Parser () Source #
Pass over this many bytes, across any number of payloads, keeping none of them.
count :: Int -> Parser a -> Parser [a] Source #
Run a recipe this many times and collect the results in order. A negative count
is a ProtocolError.
bVarChar :: Parser Text Source #
A B_VARCHAR: a string of UTF-16LE with a one-byte length in characters.
usVarChar :: Parser Text Source #
A US_VARCHAR: a string of UTF-16LE with a two-byte length in characters.
isolate :: Int -> Parser a -> Parser a Source #
Run a recipe on exactly the next n bytes, for a token that declares its own
length. The recipe can't read past them, and it is a ProtocolError if it leaves
any unread. The bytes are collected first, so this suits short tokens rather than
anything large or anything repeated per row. Compare limited.
limited :: Int -> Parser a -> Parser a Source #
Run a recipe, with a ProtocolError if it receives more than this many bytes
before it finishes. This is for a token with no overall length of its own, so that
the lengths inside it can't make the reader wait or collect without end. Unlike
isolate, it collects nothing first, and the recipe may finish early.
Value ownership and assembly
data ValueBytes Source #
A value's bytes, and whether they are cheap to keep. Borrowed bytes are a slice
of a receive buffer. Nothing overwrites them, but holding them keeps the whole buffer
alive. Owned bytes are in a buffer of their own. The difference lets a decoder
that keeps bytes copy only when it has to, and lets one that only looks at them, such
as an integer's, never copy.
Constructors
| Borrowed !ByteString | |
| Owned !ByteString |
valueBytes :: ValueBytes -> ByteString Source #
The bytes, to look at now. Keeping them may keep a receive buffer alive.
keepBytes :: ValueBytes -> ByteString Source #
The bytes, cheap to keep, because Borrowed bytes are copied.
bytesValue :: Int -> Int -> Parser (Maybe ValueBytes) Source #
convertBytes :: Int -> Int -> (ByteString -> Either ReadFailure a) -> (ByteString -> Either ReadFailure a) -> Parser a Source #
Read a value of a known length, as bytesValue does, and convert it. A value
that lies within the current payload goes to the first conversion as a slice of it,
and one that crossed payloads goes to the second, assembled in a buffer of its own.
A Left from either is a failed value (failValue) after the bytes, and a value
that a drain threw away is ReadDiscarded.
prefixedValue :: Int -> Int -> (ByteString -> Either ReadFailure a) -> Recipe env a -> Recipe env a -> Recipe env a Source #
A value that lies in the payload, converted as convertBytes converts one, or
else another recipe. Its delimiter is a length prefix of one or two bytes,
little-endian, with a value that means NULL, or no prefix and a fixed length: the
delimiter arguments are the prefix's width and the NULL value, or 0 and the length.
A NULL runs the first recipe after the prefix. A value that doesn't lie wholly in
the payload, or any other width, runs the second from the delimiter, which it must
read itself. The environment is passed to both recipes unread.
prefixedRowValue :: Int -> Int -> Int -> (ByteString -> Either ReadFailure a) -> Recipe ByteString a -> Recipe ByteString a -> Recipe ByteString a Source #
prefixedValue for a column of a row, whose environment is the NBCROW bitmap
(empty for a ROW token) and whose first argument is the column's index from
zero. A column the bitmap marks NULL has no delimiter, and runs the NULL recipe
at once.
data Accumulator Source #
accumulator :: Maybe Word64 -> Accumulator Source #
Start a value with this declared length, or with an unknown one.
append :: Int -> Accumulator -> ByteString -> IO Accumulator Source #
Add a piece to the value, which is copied, so the piece's packet can be released. Use the result in place of the argument.
The first argument bounds what a declared length alone can allocate. A server that declares a huge value and doesn't send it costs that much memory, and after that at most three times what it has actually sent. A value that turns out longer than it declared carries on as a value of unknown length.
finish :: Accumulator -> IO ByteString Source #
The assembled value, which is safe to keep. Don't use the Accumulator again.
Read failures
data ReadFailure Source #
Why a value or a row was read past without a result.
Constructors
| DecodeFailed !DecodeError | The value was read but can't be decoded, or the row doesn't have the columns its decoder expects. |
| ReadDiscarded | The response is being thrown away, so the value wasn't assembled and its consumer didn't run. Nothing reads this result. |
Instances
| Show ReadFailure Source # | |
Defined in Hmssql.Tds.Parser Methods showsPrec :: Int -> ReadFailure -> ShowS # show :: ReadFailure -> String # showList :: [ReadFailure] -> ShowS # | |
data DecodeError Source #
A row or value from the server did not fit the requested Haskell type:
a column count or SQL type the row type doesn't accept (found when the
result set's columns arrive, before any row), or a value that doesn't
decode (a NULL for a non-Maybe field, an out-of-range integer, a
refineField that said no). The rest of the response
is read before it is thrown, so the connection stays usable.
Constructors
| DecodeError | |
Fields
| |
Instances
| Exception DecodeError Source # | |
Defined in Hmssql.Tds.Parser Methods toException :: DecodeError -> SomeException # fromException :: SomeException -> Maybe DecodeError # displayException :: DecodeError -> String # backtraceDesired :: DecodeError -> Bool # | |
| Show DecodeError Source # | |
Defined in Hmssql.Tds.Parser Methods showsPrec :: Int -> DecodeError -> ShowS # show :: DecodeError -> String # showList :: [DecodeError] -> ShowS # | |
| Eq DecodeError Source # | |
Defined in Hmssql.Tds.Parser | |
newtype ProtocolError Source #
The byte stream from the server could not be interpreted. The connection is unusable afterwards.
Constructors
| ProtocolError String |
Instances
| Exception ProtocolError Source # | |
Defined in Hmssql.Tds.Parser Methods toException :: ProtocolError -> SomeException # fromException :: SomeException -> Maybe ProtocolError # displayException :: ProtocolError -> String # backtraceDesired :: ProtocolError -> Bool # | |
| Show ProtocolError Source # | |
Defined in Hmssql.Tds.Parser Methods showsPrec :: Int -> ProtocolError -> ShowS # show :: ProtocolError -> String # showList :: [ProtocolError] -> ShowS # | |
data ResultAbandoned Source #
A result stream was consumed after a newer request was started on the
same connection, which discards any unread results, or after a read of
it was interrupted by an asynchronous exception, which cancels the
statement (see queryS). Also thrown when pulling again
from a stream that was already read to its end.
On a MARS connection (connectMars) a newer request
leaves the stream open, so this is thrown only by a stream that was
canceled: by cancelResponse or the end of its
withQueryS, by cancelActiveStatement,
a reset, the rollback of a transaction it was open
in, or an interrupted read.
Constructors
| ResultAbandoned |
Instances
| Exception ResultAbandoned Source # | |
Defined in Hmssql.Tds.Parser Methods toException :: ResultAbandoned -> SomeException # fromException :: SomeException -> Maybe ResultAbandoned # displayException :: ResultAbandoned -> String # backtraceDesired :: ResultAbandoned -> Bool # | |
| Show ResultAbandoned Source # | |
Defined in Hmssql.Tds.Parser Methods showsPrec :: Int -> ResultAbandoned -> ShowS # show :: ResultAbandoned -> String # showList :: [ResultAbandoned] -> ShowS # | |