| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
Hmssql
Description
hmssql is a native Haskell client for Microsoft SQL Server (2017 and later) and Azure SQL Database. It implements SQL Server's wire protocol directly (TDS 7.4 and 8.0), so no ODBC driver or client library is needed.
The query API resembles postgresql-simple's, with one key difference: a
query and its parameters are one Query value, so query conn q params
becomes query conn q. Unlike hpgsql
(a newer PostgreSQL library that inspired this one), hmssql does not
include a quasiquoter.
This module exports what most programs need: connecting, building and running queries, streaming, transactions, and the classes that turn rows into your records and your values into parameters. The rest of the library is a module each, listed at the end of this page.
Getting started
{-# LANGUAGE DeriveAnyClass, DerivingStrategies, OverloadedStrings #-}
import Data.Text (Text)
import Data.Time (Day, fromGregorian)
import GHC.Generics (Generic)
import Hmssql
-- The columns of a SELECT, in order. A nullable column is a Maybe.
data Customer = Customer
{ customerId :: Int
, name :: Text
, birthDate :: Maybe Day
}
deriving stock (Show, Generic)
deriving anyclass (FromSqlRow)
info :: ConnectInfo
info = defaultConnectInfo
{ connectUser = "sa"
, connectPassword = "..."
, connectDatabase = "your_database"
, connectTls = defaultTlsSettings { tlsTrustServerCertificate = True }
}
main :: IO ()
main = withConnection info $ \conn -> do
-- A value never goes into the SQL text: param makes it a parameter.
customers <- query conn $
"SELECT id, name, birth_date FROM dbo.customer WHERE birth_date < "
<> param (fromGregorian 1900 1 1)
mapM_ print (customers :: [Customer])
-- Or write the SQL with @p1, @p2, ... and pass a tuple or a record.
n <- execute conn $ mkQuery
"INSERT INTO dbo.customer (name, birth_date) VALUES (@p1, @p2)"
("Ada" :: Text, Just (fromGregorian 1815 12 10))
putStrLn (show n ++ " row inserted")The columns of the SELECT match the record's fields by position.
Before the first row is decoded, each column's type is checked against
its field, and a mismatch throws DecodeError naming the
column. A tuple works in place of a record, and Only wraps a single
column.
Connections are encrypted, and by default the server's certificate is
validated against the system's certificate store. A SQL Server on
localhost uses a self-signed certificate it generated for itself, so
that validation fails, and tlsTrustServerCertificate skips it. Use
it for development only.
Connecting
Start from defaultConnectInfo, which connects to localhost on port
1433, encrypts the connection and validates the server's certificate
against the system store, and change the fields that differ.
withConnection opens a connection, runs an action with it and closes
it afterward. connect and close are the two halves, for a
connection that outlives one action.
For Azure SQL Database, set the host name, the database and the credentials, and leave the TLS settings alone: Azure's certificate validates as it is.
azure = defaultConnectInfo
{ connectHost = "myserver.database.windows.net"
, connectUser = "your_login"
, connectPassword = "..."
, connectDatabase = "your_database"
}A Connection belongs to one thread at a time, and it runs one request
at a time unless it was opened with connectMars. Give each thread
its own connection, from a pool. release cleans a connection up as
it goes back to the pool, and renew gives the next borrower a fresh
handle to it. Hmssql.Connection has a recipe for the
resource-pool package.
Building queries
A Query is SQL text together with its parameters. With
OverloadedStrings, a string literal is a query with no parameters. A
value never goes into the SQL text itself. There are two ways to pass
it as a parameter, and they mix:
"SELECT id FROM dbo.customer WHERE name = " <> param name <> " AND birth_date < " <> param cutoff
Joining renumbers the placeholders, so SQL assembled at runtime from
optional pieces always refers to the right parameters. namedParam
gives a parameter a name, escapeIdentifier quotes a table or column
name that comes from data, and rawSql splices text in as it is.
mkQuerytakes SQL text written with@p1,@p2, ... and oneToSqlRowvalue holding the parameters: a tuple, a record with a derived instance, anOnlyfor a single parameter, a list ofSqlParam, orNamedParamsfor text written with@nameplaceholders.
mkQuery "UPDATE dbo.customer SET email = @p1 WHERE id = @p2" (email, customerId)
queryText renders a query as it is sent, with @p1 in place of each
value, for logging.
A query with no parameters is sent as a SQL batch. A query with
parameters goes through sp_executesql, whose body runs in a scope of
its own, so a SET option or a #temp table it creates is gone when
it returns.
Running queries
Every function here takes the connection and a Query. They differ in
what they make of the response:
executeruns a statement and returns the number of rows it affected.execute_discards the count.queryreturns the rows of the first result set as a list.query1returns exactly one row, andqueryMayzero or one. Any other number of rows throwsUnexpectedResult.querySreturns the rows as a stream, read from the socket as they are consumed, andwithQuerySgives the stream a scope. The next section explains.queryFoldfolds over the rows as they arrive, with neither a list nor a stream.queryResultSetsruns a batch that returns several result sets and decodes each into its own row type. ItsResultSetsargument is built fromresultSetandskipResultSet.executeManyruns one statement text once for each row of parameters, in one round trip.executeStatementsruns a list of statements in one round trip.
Hmssql.Query has the rest: the With variants, which take a decoder
of your own instead of the row type's instance;
queryMulti, for result sets whose columns are known
only at runtime; callProcedure, for stored procedures
with output parameters; and the forms of executeMany that report
each statement's outcome instead of throwing the first failure.
Hmssql.BatchQuery pairs each of several statements with what to do
with its result and sends them all in one round trip.
Hmssql.BulkCopy loads many rows into one table by the protocol's
bulk load, which is the fastest way to fill a table.
Streaming
queryS returns the rows as a Stream from the streaming
package instead of as a list. A row is read from the socket when you
pull it, so memory doesn't grow with the result. Consume the stream
with the functions of Streaming.Prelude, which has the functions a
list has, such as S.mapM_, S.fold_ and S.take:
import qualified Streaming.Prelude as S withQueryS conn "SELECT id, name FROM dbo.customer" $ \rows -> S.mapM_ (\(i :: Int, name :: Text) -> print (i, name)) rows
withQueryS runs an action with the stream and cancels whatever the
action didn't read, so the stream never outlives its scope. queryS
hands you the stream itself, with one rule: read it to its end before
the connection's next request. That request cancels the statement, and
the old stream then throws ResultAbandoned. A
connection opened with connectMars keeps several streams open at
once, and Hmssql.Connection has the rules.
Each row is still read whole. To stream one large value, such as a
varbinary(max) column that holds a file, see
Hmssql.Decoding. Hmssql.Encoding sends a
large parameter the same way.
Rows and parameters
FromSqlRow turns a row into a value. Derive it for a record whose
fields are FromSqlField types, one field per column of the SELECT
in order, as Customer above does. Tuples and Only have instances
already. FromSqlField turns one column into a value, and the usual
Haskell types have instances: Int, Text, Double,
Day, LocalTime, ByteString
and so on. A nullable column is a Maybe.
Hmssql.Types has the table of which SQL types each accepts.
ToSqlRow and ToSqlField are the other direction, for parameters.
Derive ToSqlRow for a record to pass to mkQuery or executeMany:
data NewCustomer = NewCustomer { newName :: Text, newBirthDate :: Maybe Day }
deriving stock (Generic)
deriving anyclass (ToSqlRow)SqlParam wraps any parameter so that a list can mix types, and
NamedParams pairs names with such parameters.
To decode a column into a type of your own, write a FromSqlField
instance with the combinators of Hmssql.Decoding, usually
refineField over an existing decoder, for an
enumeration stored as a varchar or for a JSON column, say.
Hmssql.Encoding has the same for parameters. Hmssql.SqlValue is a
value of any SQL type, for columns and parameters whose types are known
only at runtime.
Transactions
withTransaction runs an action in a transaction. It commits if the
action returns and rolls back if it throws. withTransactionLevel
does the same at a chosen IsolationLevel. A transaction can't be
begun inside another, and a transaction the server ended under the
action, after a deadlock or an error under XACT_ABORT, is reported as
an exception rather than committed. Hmssql.Transaction has those
exceptions, and the manual begin,
commit and rollback.
Handling errors
Every exception hmssql throws is in Hmssql.Errors. After a
SqlServerError, which means the server reported an
error, or after a DecodeError,
EncodeError or UnexpectedResult, the
connection is still usable. After a ConnectionClosed,
ConnectionBroken or ProtocolError it is
not: close it and open another.
Nothing here takes a timeout argument. Interrupt a call with an
asynchronous exception instead, from timeout or
killThread. The statement is canceled on the
server, and the connection stays usable.
The other modules
- Hmssql.Connection: the connection options in full, the pool recipe, the MARS rules, and the negotiated protocol state.
- Hmssql.Query: building and running queries, in full, and the low-level request and response API.
- Hmssql.BatchQuery: several statements in one round trip, each with its own result.
- Hmssql.BulkCopy: loading many rows into one table.
- Hmssql.Transaction: manual transactions, and the transaction exceptions.
- Hmssql.Decoding and Hmssql.Encoding: decoders and encoders of your own, and streaming one large value.
- Hmssql.SqlValue: a value of any SQL type. Hmssql.Types: hmssql's own value types, and the tables of SQL and Haskell types.
- Hmssql.Errors: every exception.
- Hmssql.Catalog: a database's tables and views, with their columns, keys and foreign keys, as data. Hmssql.SqlType: SQL Server's data types by name.
- Hmssql.Query.Types: the representations of the query types, for
libraries built on hmssql.
Hmssql.Tds.*: the wire protocol.
Synopsis
- data Connection
- data ConnectInfo = ConnectInfo {
- connectHost :: !Text
- connectPort :: !Text
- connectProtocol :: !Protocol
- connectInstance :: !Text
- connectUser :: !Text
- connectPassword :: !Text
- connectAccessToken :: !(Maybe Text)
- connectDatabase :: !Text
- connectEncryption :: !Encryption
- connectTls :: !TlsSettings
- connectApplicationName :: !Text
- connectServerName :: !Text
- connectLanguage :: !Text
- connectPacketSize :: !Word32
- connectReadOnlyIntent :: !Bool
- connectSessionSetup :: ![Text]
- connectMars :: !Bool
- connectMarsWindow :: !Int
- connectPinReader :: !Bool
- connectValueAllocAhead :: !Int
- connectOnInfoMessage :: !(ServerMessage -> IO ())
- defaultConnectInfo :: ConnectInfo
- data Protocol
- withConnection :: ConnectInfo -> (Connection -> IO a) -> IO a
- connect :: ConnectInfo -> IO Connection
- close :: Connection -> IO ()
- release :: Connection -> IO Bool
- renew :: Connection -> IO Connection
- unusableReason :: Connection -> IO (Maybe UnusableReason)
- data Encryption
- data TlsSettings = TlsSettings {}
- defaultTlsSettings :: TlsSettings
- data Query
- mkQuery :: ToSqlRow p => Text -> p -> Query
- param :: ToSqlField a => a -> Query
- namedParam :: ToSqlField a => Text -> a -> Query
- escapeIdentifier :: Text -> Query
- rawSql :: Text -> Query
- queryText :: Query -> Text
- execute :: Connection -> Query -> IO Int64
- execute_ :: Connection -> Query -> IO ()
- query :: FromSqlRow r => Connection -> Query -> IO [r]
- query1 :: FromSqlRow r => Connection -> Query -> IO r
- queryMay :: FromSqlRow r => Connection -> Query -> IO (Maybe r)
- queryS :: FromSqlRow r => Connection -> Query -> IO (Stream (Of r) IO ())
- withQueryS :: FromSqlRow r => Connection -> Query -> (Stream (Of r) IO () -> IO a) -> IO a
- queryFold :: FromSqlRow r => Connection -> Query -> a -> (a -> r -> IO a) -> IO a
- executeMany :: ToSqlRow p => Connection -> Text -> [p] -> IO [Int64]
- executeStatements :: Connection -> [Query] -> IO [Int64]
- data ResultSets a
- resultSet :: FromSqlRow r => ResultSets [r]
- skipResultSet :: ResultSets ()
- queryResultSets :: ResultSets a -> Connection -> Query -> IO a
- withTransaction :: Connection -> IO a -> IO a
- withTransactionLevel :: IsolationLevel -> Connection -> IO a -> IO a
- data IsolationLevel
- class FromSqlRow a where
- rowDecoder :: RowDecoder a
- class FromSqlField a where
- class ToSqlRow a where
- rowEncoder :: RowEncoder a
- class ToSqlField a where
- fieldEncoder :: FieldEncoder a
- newtype Only a = Only {
- fromOnly :: a
- data SqlParam = ToSqlField a => SqlParam a
- newtype NamedParams = NamedParams [NamedParam]
Connections
data Connection Source #
An open connection to a server. It belongs to one thread at a time; Hmssql.Connection has the rules.
data ConnectInfo Source #
How to connect. Start from defaultConnectInfo and change the fields
that differ. Textual settings are Text, the host, port and instance
included.
Azure SQL Database accepts both the default EncryptOn, which is
TDS 7.4, and EncryptStrict, which is TDS 8.0. The server name in the
login defaults to connectHost. Under Azure's Default connection
policy, a connection from inside Azure through the public endpoint is
redirected to the node that hosts the database, on a port from 11000 to
11999, and connect follows the redirect, so those outbound ports must
be open.
azure = defaultConnectInfo
{ connectHost = "myserver.database.windows.net"
, connectUser = "your_login"
, connectPassword = "..."
, connectDatabase = "your_database"
}To log in with Entra ID instead of a SQL login, put an access token in
connectAccessToken and leave the user and password empty.
Constructors
| ConnectInfo | |
Fields
| |
defaultConnectInfo :: ConnectInfo Source #
The defaults, which the example at the top of Hmssql starts from:
localhoston port 1433, overProtocolAuto, to the default instance.- No user, password, access token or database.
EncryptOnwithdefaultTlsSettings: the whole session is encrypted, and the server's certificate is validated against the system store.- 8000-byte packets, no session setup and no MARS.
What a connection travels on (connectProtocol).
Shared Memory and Named Pipes exist on Windows only; asking for one
elsewhere makes connect throw. Both carry the same bytes as TCP, so
encryption, logins and everything after work the same. On Windows
under GHC's native I/O manager (+RTS --io-manager=native), TCP goes
through network's own calls and the pipes through the manager when
hmssql was built against network 3.3 or later with a GHC whose native
manager network supports, with or without -threaded. Without
-threaded, network's blocking calls (the lookup, the connect, and a
send once the server stops reading) and the open of a pipe stop every
Haskell thread while they block. A build that can't use the native
manager needs -threaded to run under it.
Constructors
| ProtocolAuto | On Windows: Shared Memory if A |
| ProtocolTcp | TCP, to |
| ProtocolSharedMemory | Shared Memory: a SQL Server on this machine, whatever
|
| ProtocolNamedPipes | Named Pipes, at the pipe SQL Server listens on by default. On another machine the pipe is reached over SMB, so Windows first authenticates the user running the program to that machine. |
| ProtocolCustom (IO Backend) | A byte stream of the caller's making
(Hmssql.Connection.Backend): the action connects and returns it,
and |
withConnection :: ConnectInfo -> (Connection -> IO a) -> IO a Source #
connect :: ConnectInfo -> IO Connection Source #
Open a connection and log in.
If the server can't be reached, this throws with the underlying ConnectionClosedBy
(TransportFailed e)IOException. When
ProtocolAuto tried several protocols, the exception says why each one
failed. There is no built-in connect timeout, so wrap the call in
timeout where one is needed. A host with several
addresses gives each but the last 4 seconds to answer before the next
is tried, so a timeout of 15 seconds still reaches a
working address behind a silent one.
A login the server redirects, as Azure SQL does from inside Azure and
an availability group listener does, is sent again to the host and
port named, over TCP and with the same settings otherwise, up to ten
times in a row. The eleventh redirect throws LoginRedirected, and so
does a redirect received in plaintext under EncryptOff.
close :: Connection -> IO () Source #
Close the connection. It is safe to call from any thread, and more
than once, through any handle to the connection, a released one
included. A request in progress on another thread is interrupted and
fails with , as does any later
use of the connection.ConnectionClosedBy ClosedByClient
The TLS goodbye can block if the server doesn't read. It can be
interrupted, and calling close again completes it. A closed
connection keeps under a kilobyte alive, so one that a pool still
references costs nothing.
release :: Connection -> IO Bool Source #
Reset the connection so that it can be returned to a connection pool.
Return False if the connection is not usable, because it was closed,
it is broken, or the reset itself failed, and so a later request could
not use it; that means it should be closed instead of returned to the
pool.
The reset does two things:
- It cancels the active query, as
cancelActiveStatementdoes, so that a stream that wasn't read to its end doesn't leave the connection busy. On a MARS connection, every open stream is canceled. - It marks the next request with the RESETCONNECTION bit, so that the
server resets the session before running that request. That costs no
round trip now. However, if
releaseis called in the middle of a transaction, it resets the connection immediately instead, so that the transaction's locks are released before the connection idles in the pool.
Nothing is sent to probe the server. A session the server dropped without closing the socket is found by the next request, not here.
The handle given is released whether the cleanup succeeded or not.
Further requests through it, and further reads from its streams, throw
ConnectionReleased. close, renew and read-only inspection still
work through it, and the next borrower gets a new handle from renew.
Like a request, release waits while another thread holds the
connection, and it can be interrupted. An interrupted release leaves
the connection broken, so a pool can wrap it in a timeout and treat
Nothing as False:
ok <- fromMaybe False <$> timeout 5000000 (release conn)
Calling release inside withTransaction is not
supported. Hmssql.Connection has a complete recipe for the
resource-pool package.
renew :: Connection -> IO Connection Source #
Issue a new handle to the connection, for the next borrower from a
pool, and make every earlier handle stale. A stale handle throws
ConnectionReleased, so a borrower that kept its handle past release
can't use the connection under the next borrower's feet. The connection
itself is shared, and only the handle is new. Like a request, renew waits while another
thread holds the connection. It sends nothing. The handle connect
returns needs no renew.
A connection known to be unusable is refused with the exception a
request would throw, ConnectionClosed or ConnectionBroken. The
usual case is a session the server ended while the connection idled in
the pool. The earlier handles are then left as they were, so close the
connection and borrow another. unusableReason says what is known
without throwing.
unusableReason :: Connection -> IO (Maybe UnusableReason) Source #
Whether the connection is known to be unusable, and why: it was
closed, an earlier failure broke it, or the server ended it while it
idled. Nothing means that no failure has been seen, not that the
connection is healthy. Nothing is sent to probe the server, so a
session the server dropped without closing the socket is found by the
next request. Like a request, it waits while another thread holds the
connection.
It describes the connection, not the handle. A released handle to a
healthy connection gives Nothing, and a request through that handle
still throws ConnectionReleased.
data Encryption Source #
How much of the connection is encrypted (connectEncryption).
The first four are the values of the ENCRYPTION option of PRELOGIN, where
the client and the server negotiate. EncryptStrict is TDS 8.0, which
never negotiates.
Constructors
| EncryptOff | Encrypt the login packet only, or the whole session if the server requires it. If the server doesn't support encryption at all, nothing is encrypted, not even the password. (0x00) |
| EncryptOn | Encrypt the whole session, and fail if the server doesn't support encryption. The default. (0x01) |
| EncryptNotSup | Encrypt nothing, and fail if the server requires encryption. (0x02) |
| EncryptReq | Encryption required. It behaves as |
| EncryptStrict | TDS 8.0: TLS from the first byte, before any PRELOGIN, and a failure against a server that doesn't support TDS 8.0. (0x04, which is never sent, since there is no PRELOGIN to send it in) |
Instances
| Show Encryption Source # | |
Defined in Hmssql.Tds.Client Methods showsPrec :: Int -> Encryption -> ShowS # show :: Encryption -> String # showList :: [Encryption] -> ShowS # | |
| Eq Encryption Source # | |
Defined in Hmssql.Tds.Client | |
data TlsSettings Source #
How the server's certificate is validated, in every mode that uses TLS.
defaultTlsSettings validates against the system store; the fields add
trust, override the name checked, or turn validation off.
Constructors
| TlsSettings | |
Fields
| |
defaultTlsSettings :: TlsSettings Source #
Validate the server's certificate against the system store, for the host that was connected to, with no extra certificates trusted.
Building queries
SQL text together with the parameters it refers to. Build one from a
string literal, with mkQuery, or from pieces joined with
<>; Hmssql.Query shows each way.
The positional parameters are sent as @p1, @p2, ... in the order
queryParams lists them, and the named parameters by name. <>
appends the text and the parameters of two queries and renumbers the
right operand's placeholders past the left operand's positional
parameters, so any two queries can be joined. A placeholder that
refers to no parameter makes sending the query throw
EncodeError.
A query with no parameters is sent as a plain SQL batch, and one with
parameters through sp_executesql. fromString (OverloadedStrings)
is text as it is, with no parameters. Show is the text as it would
be sent, without the parameter values.
mkQuery :: ToSqlRow p => Text -> p -> Query Source #
SQL text with its parameters. The parameters are those of the
ToSqlRow value: a tuple, a record with a derived instance, an
Only for a single parameter, a list of SqlParam, or
NamedParams. In the text, @p1, @p2, ... refer to the positional
parameters and @name to the named ones.
mkQuery "UPDATE dbo.customer SET email = @p1 WHERE id = @p2" (email, customerId)
@pN, in either case, is a placeholder wherever it appears outside a
string literal, a quoted identifier or a comment, even where the SQL
means something else by it. So a variable of that name, as in
DECLARE @p1 int, can't be used in a mkQuery text; use another name.
A placeholder that refers to no parameter makes sending the query throw
EncodeError.
A string literal (OverloadedStrings) is the same as
for text without placeholders.mkQuery text ()
The text is lexed once per application of mkQuery to it, not once per
set of parameter values. With a literal, the lexing floats out to the
top level when mkQuery is inlined, so the text is lexed once per
program run however many times the query is built with new parameters.
param :: ToSqlField a => a -> Query Source #
A positional parameter: a placeholder in the text, and the value sent with the query.
"WHERE id = " <> param (42 :: Int)
namedParam :: ToSqlField a => Text -> a -> Query Source #
A named parameter: @name in the text, and the value sent with the
query. The leading @ is optional. The name must be a T-SQL variable
name (see paramNameProblem), and not of the form @pN, which is
reserved for the positional parameters. A query that uses one name for
two parameters can't be sent, and throws EncodeError.
"OFFSET " <> namedParam "skip" (100 :: Int) <> " ROWS"
escapeIdentifier :: Text -> Query Source #
An identifier (a table or column name from data, say) quoted with
brackets: [name], with ] doubled.
"SELECT " <> escapeIdentifier col <> " FROM " <> escapeIdentifier table
rawSql :: Text -> Query Source #
Text spliced into the query as it is, with no parameters; a string
literal gives the same. Nothing in it is parsed, so a @p1 in it is
left to the server. For text that comes from data, use param,
namedParam or escapeIdentifier instead.
queryText :: Query -> Text Source #
The SQL text as it is sent, with @pN for the placeholders.
A placeholder next to a character that could continue a variable name
gets a space between them: "x = " <> param 1 <> "1" is sent as
x = @p1 1, not x = @p11 (another parameter), and "N" <> param s
as N @p1, not the identifier N@p1.
Running queries
execute :: Connection -> Query -> IO Int64 Source #
Run a statement, or a batch of them, and return the number of rows affected, which is the sum of the counts the server reports. Any result sets are read and discarded.
execute_ :: Connection -> Query -> IO () Source #
Run a statement as execute does, and discard the row count. The
whole response is still read, and a server error is still thrown.
query :: FromSqlRow r => Connection -> Query -> IO [r] Source #
The rows of the first result set, decoded into the row type by its
FromSqlRow instance, with the column types checked before the first
row. A query with no result set, such as an UPDATE, gives []. The
rest of the response is read to its end, so a later result set is
discarded, and an error from a later statement is still thrown.
query1 :: FromSqlRow r => Connection -> Query -> IO r Source #
query for exactly one row. It throws UnexpectedResult if the
first result set has no rows or several.
queryMay :: FromSqlRow r => Connection -> Query -> IO (Maybe r) Source #
query for at most one row. It returns Nothing if the first result
set has no rows, and throws UnexpectedResult if it has several.
queryS :: FromSqlRow r => Connection -> Query -> IO (Stream (Of r) IO ()) Source #
The rows of the first result set as a Stream, read from the socket
as they are consumed, so memory doesn't grow with the result.
Hmssql.Query shows how to consume one.
Read the stream to its end before making another request on the
connection. The next request cancels the statement and discards what
was left, and the old stream then throws ResultAbandoned. So does a
stream whose read was interrupted, by timeout say,
which cancels the statement at once, and a stream run a second time
after it was drained.
A stream not read to its end leaves its statement running on the
server, holding its locks, until the next request,
cancelActiveStatement or
release cancels it. On a MARS connection
(connectMars) other requests leave the stream open
instead, so it stays open until it is read to its end, canceled with
cancelActiveStatement, or the connection is
released. withQueryS is the form that can't leak a stream.
Canceling a statement that is still running inside a transaction under
SET XACT_ABORT ON rolls the transaction back
(TransactionEndedEarly). Where the transaction
must survive, read the stream to its end.
withQueryS :: FromSqlRow r => Connection -> Query -> (Stream (Of r) IO () -> IO a) -> IO a Source #
queryS in a scope. The action gets the stream, and when the action
ends, a stream not read to its end is canceled (cancelResponse).
When the action returns, or throws an exception of its own,
withQueryS returns once the statement is over on the server and its
locks are released. On a MARS connection
(connectMars) this is the way to read a stream
beside other requests. Without MARS it works too, under the rule of
queryS: a request the action makes abandons the stream.
withQueryS conn "SELECT id FROM dbo.parent" $ \parents ->
S.mapM_ (\(Only pid) -> query conn ("SELECT ... WHERE parent_id = " <> param (pid :: Int))) parentsAn interruption (timeout, killThread)
doesn't wait. The statement is canceled at once, and the connection's
next request reads the rest of the response first; a transaction
function, release and
cancelActiveStatement do the same. Until then the
statement's locks may still be held, and
cancelActiveStatement waits for them to go. The
stream must not be used after the action.
queryFold :: FromSqlRow r => Connection -> Query -> a -> (a -> r -> IO a) -> IO a Source #
Fold over the rows of the first result set as they arrive, without
building a list or a stream: the fastest way to count, sum or write
rows out. The step runs with the connection lock held, in the middle
of reading the response, so it must not use the connection. On a MARS
connection it may, because the request takes a session of its own.
An exception from the step cancels the response at once and
propagates. If it is the step's own exception, the response is
drained before it propagates; if it is an interruption, the drain is
left to the next request. After the fold, the rest of the response is
read to its end, as with query.
executeMany :: ToSqlRow p => Connection -> Text -> [p] -> IO [Int64] Source #
Run one statement text once for each row of parameters, all in one
request, and return the number of rows each affected, in order. The
text refers to a row's parameters as a mkQuery text does, with
@p1, @p2, ..., and a row is any ToSqlRow value.
executeMany conn "INSERT INTO item (id, name) VALUES (@p1, @p2)" [(1 :: Int, "one" :: Text), (2, "two")]
Each row is its own sp_executesql call, with the text and the
parameter declaration shared (read and encoded once, not per row), so
the server compiles the statement once and reuses the plan. It is
with that
sharing, and everything executeStatements conn (map (mkQuery sql) rows)executeStatements says about round trips,
errors, transactions and interruption applies: statements after a
failed row may have run, so use withTransaction
for all or nothing, or executeManyOutcomes to see which rows failed.
For a large load into one table, bulkInsert is
faster.
executeStatements :: Connection -> [Query] -> IO [Int64] Source #
Run several statements in one request, which is one round trip, and
return the number of rows each affected, in order, as execute would
return it. Each statement is sent as its own sp_executesql call, all
in one RPC message. That includes a statement with no parameters, so a
SET option it sets or a #temp table it creates lasts only until it
returns; send such statements with execute. For many rows of one
statement, executeMany is the same with the text read once. To know
which statements failed, use executeStatementsOutcomes.
The server runs the statements in order. If some fail, the whole
response is still read, and then SqlServerError is thrown for the
first that failed. Statements after a failed one may have run: an
error that only ends its statement (a constraint violation, say) lets
the server go on; one that aborts the batch (THROW, a conversion
error, a deadlock, any error under SET XACT_ABORT ON) ends the
request there. For all-or-nothing, run it inside
withTransaction.
A statement that can't be sent (see queryRequest) throws
EncodeError naming its position, before its message is sent. A long
list goes out in messages of about 1 MiB, each read before the next is
sent, so memory stays bounded; the split changes nothing else.
Interrupted, it cancels the running statement; the statements before
it have run. An empty list returns [] without a round trip.
Several result sets
data ResultSets a Source #
How to decode a response that holds several result sets, one at a
time in order, for queryResultSets. Build one from
resultSet and skipResultSet with the
Applicative or Monad combinators; as a Monad, a later decoder may
depend on earlier rows. Statements that produce no result set, such as
DECLARE, SET and UPDATE, are skipped over.
(parents, children) <- queryResultSets ((,) <$> resultSet <*> resultSet) conn "SELECT id, name FROM dbo.parent; SELECT id, parent_id FROM dbo.child"
Instances
| Applicative ResultSets Source # | |
Defined in Hmssql.Query.Types Methods pure :: a -> ResultSets a # (<*>) :: ResultSets (a -> b) -> ResultSets a -> ResultSets b # liftA2 :: (a -> b -> c) -> ResultSets a -> ResultSets b -> ResultSets c # (*>) :: ResultSets a -> ResultSets b -> ResultSets b # (<*) :: ResultSets a -> ResultSets b -> ResultSets a # | |
| Functor ResultSets Source # | |
Defined in Hmssql.Query.Types Methods fmap :: (a -> b) -> ResultSets a -> ResultSets b # (<$) :: a -> ResultSets b -> ResultSets a # | |
| Monad ResultSets Source # | |
Defined in Hmssql.Query.Types Methods (>>=) :: ResultSets a -> (a -> ResultSets b) -> ResultSets b # (>>) :: ResultSets a -> ResultSets b -> ResultSets b # return :: a -> ResultSets a # | |
resultSet :: FromSqlRow r => ResultSets [r] Source #
The rows of the next result set, as query would return them. It
throws UnexpectedResult if the response has no further result set.
skipResultSet :: ResultSets () Source #
Read the next result set and discard its rows. It throws
UnexpectedResult if the response has no further result set.
queryResultSets :: ResultSets a -> Connection -> Query -> IO a Source #
Run a batch that returns several result sets, and decode them one at a time, each into its own row type.
(parents, children) <- queryResultSets ((,) <$> resultSet <*> resultSet) conn "SELECT id, name FROM dbo.parent; SELECT id, parent_id FROM dbo.child"
The whole response is read, so an error in a later statement still
throws SqlServerError, and result sets the ResultSets didn't ask
for are discarded.
Transactions
withTransaction :: Connection -> IO a -> IO a Source #
Run the action in a transaction. It commits if the action returns,
and rolls back if the action throws, an asynchronous exception from
killThread or timeout included.
Beginning the transaction is a round trip of its own before the action
runs, and the COMMIT is another.
The connection is held for the whole transaction, so requests from other threads wait until it ends instead of running inside it. The action must not wait for another thread that uses the connection.
On a MARS connection (connectMars) every request
the body makes, and every stream it opens, runs in the transaction. No
response may be unfinished when it begins or when the body returns
(TransactionRequestsActive): begin first, open the streams inside
with withQueryS, and let them end before the body
returns. If the body throws, the responses it left open are canceled
before the rollback.
Throws TransactionAlreadyActive if a transaction is already active;
TransactionEndedEarly (after cleaning up) if the body returns but the
transaction it started is gone; and TransactionStillOpen (after
rolling everything back) if the body began a nested transaction and
didn't end it. If the rollback after a failure itself fails, the
connection is marked broken (RollbackFailed) rather than
left in an unknown state, and the exception that caused the rollback
propagates, unless an asynchronous exception interrupted the rollback.
withTransactionLevel :: IsolationLevel -> Connection -> IO a -> IO a Source #
withTransaction at the given isolation level. The begin request
sets the level, and the COMMIT or ROLLBACK batch sets it back to
READ COMMITTED, the session's level at login, however the transaction
ends, so no round trips are added. The session is assumed to be at
READ COMMITTED outside withTransactionLevel, and a level the
application set by hand is not preserved.
data IsolationLevel Source #
SQL Server's transaction isolation levels, for withTransactionLevel.
ReadCommitted is the session's level at login. Snapshot needs
ALLOW_SNAPSHOT_ISOLATION turned on for the database, or the
transaction fails.
Constructors
| ReadUncommitted | |
| ReadCommitted | |
| RepeatableRead | |
| Snapshot | |
| Serializable |
Instances
| Bounded IsolationLevel Source # | |
Defined in Hmssql.Transaction | |
| Enum IsolationLevel Source # | |
Defined in Hmssql.Transaction Methods succ :: IsolationLevel -> IsolationLevel # pred :: IsolationLevel -> IsolationLevel # toEnum :: Int -> IsolationLevel # fromEnum :: IsolationLevel -> Int # enumFrom :: IsolationLevel -> [IsolationLevel] # enumFromThen :: IsolationLevel -> IsolationLevel -> [IsolationLevel] # enumFromTo :: IsolationLevel -> IsolationLevel -> [IsolationLevel] # enumFromThenTo :: IsolationLevel -> IsolationLevel -> IsolationLevel -> [IsolationLevel] # | |
| Show IsolationLevel Source # | |
Defined in Hmssql.Transaction Methods showsPrec :: Int -> IsolationLevel -> ShowS # show :: IsolationLevel -> String # showList :: [IsolationLevel] -> ShowS # | |
| Eq IsolationLevel Source # | |
Defined in Hmssql.Transaction Methods (==) :: IsolationLevel -> IsolationLevel -> Bool # (/=) :: IsolationLevel -> IsolationLevel -> Bool # | |
Rows and parameters
class FromSqlRow a where Source #
Types that decode from a whole row, one column per field in order.
Tuples of up to twelve fields and Only have instances. Derive one for
a record whose fields are FromSqlField types, one field per column of
the SELECT, in order:
data Customer = Customer { customerId :: Int, name :: Text, birthDate :: Maybe Day }
deriving stock (Show, Generic)
deriving anyclass (FromSqlRow)The column types are checked against the fields before the first row is
decoded, and a mismatch is a DecodeError naming the column. For a row
decoder written by hand, see singleField.
Minimal complete definition
Nothing
Methods
rowDecoder :: RowDecoder a Source #
default rowDecoder :: (Generic a, GRowDecoder (Rep a)) => RowDecoder a Source #
Instances
class FromSqlField a where Source #
Types that decode from one column. The usual Haskell types have
instances, and Hmssql.Types has the table of which SQL types each
accepts. Maybe accepts NULL; without it, a NULL is a DecodeError.
An instance of your own defines fieldDecoder, usually with
refineField over a built-in type's decoder.
Minimal complete definition
Methods
fieldDecoder :: FieldDecoder a Source #
The decoder of one column of this type.
fieldRowDecoder :: RowDecoder a Source #
A row decoder of one column of this type, the same as . Tuples, singleField
fieldDecoderOnly and derived FromSqlRow instances read their
columns with it, and the built-in types' are faster than singleField. An
instance doesn't define it.
Instances
class ToSqlRow a where Source #
Types that can be sent as a list of parameters: the @p1, @p2,
... of mkQuery, or a row of executeMany or
bulkInsert. Derive it for a record whose fields are
ToSqlField types, one parameter per field in order:
data NewCustomer = NewCustomer { newName :: Text, newBirthDate :: Maybe Day }
deriving stock (Generic)
deriving anyclass (ToSqlRow)Tuples of up to ten fields, (), Only, a list of one ToSqlField
type, a list of SqlParam to mix types, and NamedParams have
instances.
Minimal complete definition
Nothing
Methods
rowEncoder :: RowEncoder a Source #
default rowEncoder :: (Generic a, GRowEncoder (Rep a)) => RowEncoder a Source #
Instances
class ToSqlField a where Source #
Types that can be sent as one parameter, through param
or as a field of a ToSqlRow value. The usual Haskell types have
instances, and Hmssql.Types has the table of which SQL type each is
sent as. An instance of your own defines fieldEncoder, usually with
contramap over a built-in type's encoder.
Methods
fieldEncoder :: FieldEncoder a Source #
Instances
| ToSqlField Decimal Source # |
|
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder Decimal Source # fieldParam :: Decimal -> FieldParams -> FieldParams | |
| ToSqlField ByteString Source # | Any length of bytes. A value of up to 8000 bytes is declared
|
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder ByteString Source # fieldParam :: ByteString -> FieldParams -> FieldParams | |
| ToSqlField ByteString Source # | Declared as a strict |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder ByteString Source # fieldParam :: ByteString -> FieldParams -> FieldParams | |
| ToSqlField Int16 Source # | |
Defined in Hmssql.Encoding | |
| ToSqlField Int32 Source # | |
Defined in Hmssql.Encoding | |
| ToSqlField Int64 Source # | |
Defined in Hmssql.Encoding | |
| ToSqlField Int8 Source # |
|
Defined in Hmssql.Encoding | |
| ToSqlField Word8 Source # |
|
Defined in Hmssql.Encoding | |
| ToSqlField RawParam Source # | |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder RawParam Source # fieldParam :: RawParam -> FieldParams -> FieldParams | |
| ToSqlField SqlParam Source # | |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder SqlParam Source # fieldParam :: SqlParam -> FieldParams -> FieldParams | |
| ToSqlField StreamParam Source # | |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder StreamParam Source # fieldParam :: StreamParam -> FieldParams -> FieldParams | |
| ToSqlField SqlValue Source # | Each constructor is sent as the wire type |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder SqlValue Source # fieldParam :: SqlValue -> FieldParams -> FieldParams | |
| ToSqlField DateTimeOffset Source # |
|
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder DateTimeOffset Source # fieldParam :: DateTimeOffset -> FieldParams -> FieldParams | |
| ToSqlField Money Source # |
|
Defined in Hmssql.Encoding | |
| ToSqlField Text Source # | Any length of string. A value of up to 4000 UTF-16 code units is declared
|
Defined in Hmssql.Encoding | |
| ToSqlField Text Source # | Declared as |
Defined in Hmssql.Encoding | |
| ToSqlField Day Source # |
|
Defined in Hmssql.Encoding | |
| ToSqlField UTCTime Source # |
|
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder UTCTime Source # fieldParam :: UTCTime -> FieldParams -> FieldParams | |
| ToSqlField LocalTime Source # |
|
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder LocalTime Source # fieldParam :: LocalTime -> FieldParams -> FieldParams | |
| ToSqlField TimeOfDay Source # |
|
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder TimeOfDay Source # fieldParam :: TimeOfDay -> FieldParams -> FieldParams | |
| ToSqlField ZonedTime Source # | As |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder ZonedTime Source # fieldParam :: ZonedTime -> FieldParams -> FieldParams | |
| ToSqlField UUID Source # | |
Defined in Hmssql.Encoding | |
| ToSqlField Integer Source # |
|
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder Integer Source # fieldParam :: Integer -> FieldParams -> FieldParams | |
| ToSqlField String Source # | |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder String Source # fieldParam :: String -> FieldParams -> FieldParams | |
| ToSqlField Bool Source # | |
Defined in Hmssql.Encoding | |
| ToSqlField Double Source # | |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder Double Source # fieldParam :: Double -> FieldParams -> FieldParams | |
| ToSqlField Float Source # | |
Defined in Hmssql.Encoding | |
| ToSqlField Int Source # | |
Defined in Hmssql.Encoding | |
| ToSqlField a => ToSqlField (Maybe a) Source # | |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder (Maybe a) Source # fieldParam :: Maybe a -> FieldParams -> FieldParams | |
The 1-tuple type or single-value "collection".
This type is structurally equivalent to the
Identity type, but its intent is more
about serving as the anonymous 1-tuple type missing from Haskell for attaching
typeclass instances.
Parameter usage example:
encodeSomething (Only (42::Int))Result usage example:
xs <- decodeSomething
forM_ xs $ \(Only id) -> {- ... -}Instances
| Functor Only # | |||||
| NFData a => NFData (Only a) # | |||||
Defined in Data.Tuple.Only | |||||
| Data a => Data (Only a) # | |||||
Defined in Data.Tuple.Only Methods gfoldl :: (forall d b. Data d => c (d -> b) -> d -> c b) -> (forall g. g -> c g) -> Only a -> c (Only a) # gunfold :: (forall b r. Data b => c (b -> r) -> c r) -> (forall r. r -> c r) -> Constr -> c (Only a) # toConstr :: Only a -> Constr # dataTypeOf :: Only a -> DataType # dataCast1 :: Typeable t => (forall d. Data d => c (t d)) -> Maybe (c (Only a)) # dataCast2 :: Typeable t => (forall d e. (Data d, Data e) => c (t d e)) -> Maybe (c (Only a)) # gmapT :: (forall b. Data b => b -> b) -> Only a -> Only a # gmapQl :: (r -> r' -> r) -> r -> (forall d. Data d => d -> r') -> Only a -> r # gmapQr :: forall r r'. (r' -> r -> r) -> r -> (forall d. Data d => d -> r') -> Only a -> r # gmapQ :: (forall d. Data d => d -> u) -> Only a -> [u] # gmapQi :: Int -> (forall d. Data d => d -> u) -> Only a -> u # gmapM :: Monad m => (forall d. Data d => d -> m d) -> Only a -> m (Only a) # gmapMp :: MonadPlus m => (forall d. Data d => d -> m d) -> Only a -> m (Only a) # gmapMo :: MonadPlus m => (forall d. Data d => d -> m d) -> Only a -> m (Only a) # | |||||
| Generic (Only a) # | |||||
Defined in Data.Tuple.Only Associated Types
| |||||
| Read a => Read (Only a) # | |||||
| Show a => Show (Only a) # | |||||
| Eq a => Eq (Only a) # | |||||
| Ord a => Ord (Only a) # | |||||
| FromSqlField a => FromSqlRow (Only a) Source # | |||||
Defined in Hmssql.Decoding Methods rowDecoder :: RowDecoder (Only a) Source # | |||||
| ToSqlField a => ToSqlRow (Only a) Source # | |||||
Defined in Hmssql.Encoding Methods rowEncoder :: RowEncoder (Only a) Source # | |||||
| type Rep (Only a) # | |||||
Defined in Data.Tuple.Only | |||||
Any value with a ToSqlField instance, so a parameter list can mix types.
Constructors
| ToSqlField a => SqlParam a |
Instances
| ToSqlField SqlParam Source # | |
Defined in Hmssql.Encoding Methods fieldEncoder :: FieldEncoder SqlParam Source # fieldParam :: SqlParam -> FieldParams -> FieldParams | |
newtype NamedParams Source #
Parameters that the SQL text refers to by name. The leading @ is
optional. A name must be a T-SQL variable name, and not of the form
@pN, which is reserved for the positional parameters; the query
functions check both before sending anything. Bulk copy matches the
names, with that one @ removed, against the table's columns
(Hmssql.BulkCopy).
mkQuery "UPDATE dbo.customer SET email = @email WHERE id = @id"
(NamedParams [("email", SqlParam newEmail), ("id", SqlParam customerId)])Constructors
| NamedParams [NamedParam] |
Instances
| ToSqlRow NamedParams Source # | |
Defined in Hmssql.Encoding Methods | |