hmssql
Safe HaskellNone
LanguageHaskell2010

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) bVarChar

chunk 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

Recipes and their driver

type Parser = Recipe () Source #

A recipe with no environment, which is the usual kind.

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.

runParser :: Parser a -> ByteString -> Either (Stream Await IO (Got a)) (Got a) Source #

Start a recipe on a payload. The result is Right if it finished within the payload, and otherwise Left with the suspended computation. This lets a loop handle the common case without a Stream.

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.

data Got a Source #

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 

data At a Source #

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 

data Await next Source #

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 Runner for its pulls, outside any mask the driver holds for reading, and gives its result to the function, which gives the rest of the recipe. A driver that is throwing the response away gives Nothing instead of running the code. An exception from the code isn't given to the function: it ends the driver, as an exception of the caller's does. The Bool says whether the code pulls the value from the response (consume), so that it may wait for the server, or has the value in hand (consumeWhole) and never pulls.

Instances

Instances details
Functor Await Source # 
Instance details

Defined in Hmssql.Tds.Parser

Methods

fmap :: (a -> b) -> Await a -> Await b #

(<$) :: a -> Await b -> Await a #

data Feed Source #

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 chunkOrDiscard.

EndOfMessage

There are no more payloads. This is the normal way to finish at a token boundary (peekToken), and a ProtocolError anywhere else.

feedBytes :: Feed -> Maybe ByteString Source #

The payload, for a read that treats More and Discard alike, or Nothing at the end of the message.

newtype Runner Source #

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.

Constructors

Runner 

Fields

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.

effect :: IO a -> Parser a Source #

Run a short piece of IO as part of a recipe, and evaluate its result. It runs once, under whatever mask the driver holds, so it must not block. Code that can block, such as a caller's consumer, belongs in consume instead.

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, [])

data Recipe env a Source #

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.

Instances

Instances details
Applicative (Recipe env) Source # 
Instance details

Defined in Hmssql.Tds.Parser

Methods

pure :: a -> Recipe env a #

(<*>) :: Recipe env (a -> b) -> Recipe env a -> Recipe env b #

liftA2 :: (a -> b -> c) -> Recipe env a -> Recipe env b -> Recipe env c #

(*>) :: Recipe env a -> Recipe env b -> Recipe env b #

(<*) :: Recipe env a -> Recipe env b -> Recipe env a #

Functor (Recipe env) Source # 
Instance details

Defined in Hmssql.Tds.Parser

Methods

fmap :: (a -> b) -> Recipe env a -> Recipe env b #

(<$) :: a -> Recipe env b -> Recipe env a #

Monad (Recipe env) Source # 
Instance details

Defined in Hmssql.Tds.Parser

Methods

(>>=) :: Recipe env a -> (a -> Recipe env b) -> Recipe env b #

(>>) :: Recipe env a -> Recipe env b -> Recipe env b #

return :: a -> Recipe env a #

MonadFail (Recipe env) Source # 
Instance details

Defined in Hmssql.Tds.Parser

Methods

fail :: String -> Recipe env a #

ask :: Recipe env env Source #

The value given to withEnv. This reads no input.

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 #

Use a Parser, such as one of the reads below, inside a Recipe.

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.

word8 :: Parser Word8 Source #

One byte.

word16le :: Parser Word16 Source #

A little-endian 16-bit integer, as most integers in TDS are.

word32le :: Parser Word32 Source #

A little-endian 32-bit integer.

word64le :: Parser Word64 Source #

A little-endian 64-bit integer.

word32be :: Parser Word32 Source #

A big-endian 32-bit integer.

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 #

chunk for a recipe that collects a value. It gives Nothing if the response started being thrown away (Discard) while this read was waiting for input. The recipe should then release what it has collected and skip the rest of the value, because Nothing reads no bytes.

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.

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 #

A value of a known length, given the allocation bound of bytes and the length. Nothing means that the response started being thrown away while the value was being assembled. The partial value has been released and its remaining bytes skipped, so the recipe can carry on with what follows.

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 #

A long value being assembled from pieces as they arrive. Use each one only once, because append may write into the same memory and its result replaces its argument. A recipe holding one must not be run twice, as runFrom explains.

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

Instances details
Show ReadFailure Source # 
Instance details

Defined in Hmssql.Tds.Parser

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

newtype ProtocolError Source #

The byte stream from the server could not be interpreted. The connection is unusable afterwards.

Constructors

ProtocolError String 

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