| Safe Haskell | None |
|---|---|
| Language | Haskell2010 |
Hmssql.Connection
Description
Opening and closing connections.
A connection is for one thread at a time, with one request outstanding
at a time (unless it is a MARS connection, below). Give each thread
its own, from a pool: release returns a connection to the pool clean,
and renew hands the next borrower a fresh handle.
Hmssql.Connection has a recipe for resource-pool.
MARS: several responses open at once
With connectMars, a connection can have several responses open at
once (SQL Server's Multiple Active Result Sets): a request made while a
queryS stream is being read leaves the stream open,
where without MARS it abandons it. Nothing else in the API changes.
Each request quietly takes a session of the connection and gives it
back when its response has been read to the end or canceled;
connectionMars says whether a connection negotiated MARS.
withQueryS conn "SELECT id, name FROM dbo.parent" $ \parents ->
S.mapM_ (\(pid, name) -> do
children <- query conn ("SELECT name FROM dbo.child WHERE parent_id = " <> param (pid :: Int))
report name children) parentsThese rules come with it:
- Open streams with
withQueryS, which cancels a stream its action didn't finish. An open stream holds a session and its statement's locks, so every stream needs an end. A plainquerySstream stays open until it is read to its end or untilcancelResponse,cancelActiveStatementorreleasecancels it. - Begin a transaction first and open the streams inside it, then finish
or cancel every one before the body returns. SQL Server refuses to
begin or commit a transaction beside an unfinished request, and a
refused commit loses the transaction, so hmssql checks first and throws
TransactionRequestsActive. A stream has finished when it has returned, so having seen its last row isn't enough.
withTransaction conn $
withQueryS conn "SELECT id, price FROM dbo.product WHERE category = 'tea'" $ \products ->
flip S.mapM_ products $ \(pid :: Int, price :: Decimal) ->
execute conn (mkQuery "UPDATE dbo.product SET price = @p1 WHERE id = @p2" (price * 1.1, pid))- Write to a table under an open stream only inside a transaction, or
with
READ_COMMITTED_SNAPSHOTon the database. Otherwise the write waits for the locks of the open scan, which can't be released while your thread waits, and it ends as a deadlock victim (error 1205), at the lock timeout (error 1222), or never. Set a lock timeout inconnectSessionSetup, so that this mistake is an error rather than a hang. Reads beside a stream, and writes to other tables, need nothing. - Change session settings (
SET,USE) with no stream open, or inconnectSessionSetup. SQL Server undoes a change made beside a running stream when that stream ends, which includes the isolation level andXACT_ABORT.withTransactionLevel,#temptables andSESSION_CONTEXTare safe. - Read a stream to its end where a transaction must survive under
SET XACT_ABORT ON. Canceling a statement that is still running can roll the transaction back (TransactionEndedEarly). - MARS interleaves; it doesn't run statements side by side, and the connection is still for one thread at a time.
- Large parameters and bulk loads belong on a connection of their own.
- On Azure SQL Database, count open streams as requests, because the Basic tier allows 30 at once. Under load, a cancel right behind its request can occasionally end the connection, and a pool replaces it.
A transaction at Snapshot isolation gives every stream in it the same
snapshot, taken at the transaction's first read of a table. That keeps
several streams consistent while other connections write, and it needs
ALLOW_SNAPSHOT_ISOLATION on the database.
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
- defaultSessionSetup :: [Text]
- 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)
- cancelActiveStatement :: Connection -> IO ()
- reset :: Connection -> IO ()
- data Encryption
- data TlsSettings = TlsSettings {}
- defaultTlsSettings :: TlsSettings
- connectionContext :: Connection -> IO TdsContext
- connectionMars :: Connection -> Bool
- data TdsContext = TdsContext {
- tdsVersion :: !TdsVersion
- tdsPacketSize :: !Word32
- tdsTransaction :: !(Maybe TransactionDescriptor)
- tdsUtf8Support :: !Bool
- tdsCollation :: !(Maybe ByteString)
- data TdsVersion
Connecting
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 |
defaultSessionSetup :: [Text] Source #
The default session setup: none. The login already sets what
Microsoft's drivers set (the ANSI options on, IMPLICIT_TRANSACTIONS
off, TEXTSIZE unlimited). The one option they leave off and SQL
Server Management Studio turns on is ARITHABORT, which only matters
as a plan-cache key; to share plans with SSMS, opt in:
defaultConnectInfo{connectSessionSetup= ["SET ARITHABORT ON"] }
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.
Returning a connection to a pool
Give each thread its own connection from a pool. With the resource-pool
package, take a connection out with renew, and give it back with release
whether your action returned or threw. A connection the server ended while it
sat in the pool is refused by renew, so it is destroyed and the next one taken:
import Control.Concurrent (forkIO)
import Control.Exception (SomeAsyncException, SomeException, finally, fromException, mask, onException, throwIO, try)
import Control.Monad (void)
import Data.Maybe (fromMaybe)
import Data.Pool
import System.Timeout (timeout)
withPooled :: Pool Connection -> (Connection -> IO a) -> IO a
withPooled pool act = mask $ \restore -> do
(conn, local) <- borrow
restore (act conn) `finally` giveBack local conn
where
borrow = do
(conn0, local) <- takeResource pool
try (renew conn0) >>= \case
Right conn -> pure (conn, local)
Left (e :: SomeException)
| Just (_ :: SomeAsyncException) <- fromException e -> putResource local conn0 >> throwIO e
| otherwise -> close conn0 >> destroyResource pool local conn0 >> borrow
giveBack local conn = void $ forkIO $ do
let discard = close conn >> destroyResource pool local conn
ok <- fromMaybe False <$> timeout 5000000 (release conn) `onException` discard
if ok then putResource local conn else discardThis is used rather than withResource because an exception from your code, such
as a timeout or a client hanging up, usually leaves the connection
fine. release keeps such a connection, where withResource would destroy it and
pay for a new login.
release runs on a thread of its own because canceling a stream is a round trip,
which the caller shouldn't wait for. The timeout bounds it. A
release that times out leaves a broken connection, so Nothing counts as False.
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.
cancelActiveStatement :: Connection -> IO () Source #
Cancel the statement whose response the connection is still
receiving, if any, and discard what remains of the response. On a MARS
connection, every open response, in one round trip. This is the first
step of release; cancelResponse cancels one response.
A queryS stream whose consumer stopped early leaves its
statement running on the server, holding its locks, until something
cancels it, and an interrupted request (timeout around
query or withQueryS) is canceled at
once but drained only by the connection's next operation, so its locks
may be held until then. This is the way to wait for them to go: if the
response has been read to its end, nothing is sent; if its last packet
has arrived, the rest is discarded locally; otherwise an Attention is
sent (none if an interruption sent it already) and the response read
up to the server's acknowledgement. When it returns, no response is
pending and the connection is not known to be broken or closed; it
throws ConnectionBroken or ConnectionClosed
otherwise, as a request would. A stream of the canceled response
throws ResultAbandoned if read on.
SQL Server treats an Attention like a timeout: a statement still
running is rolled back, and under SET XACT_ABORT ON so is the
transaction it runs in (withTransaction then
throws TransactionEndedEarly).
This is not a way to interrupt a request from another thread: it takes
the connection's lock, so it waits for a request in progress.
Interrupt the thread instead (timeout,
killThread), which sends the Attention at once.
If the drain is interrupted, the connection is broken.
reset :: Connection -> IO () Source #
Reset the session before the next request. A pool should call
release instead, which does this and more.
Nothing is sent now: the next request carries the RESETCONNECTION bit,
which does what sp_reset_connection does without a round trip of its
own. The server returns the session to its state right after login: an
open transaction is rolled back, #temp tables and cursors are
dropped, and SET options revert. The session setup
(connectSessionSetup) then runs again; with a non-empty setup, one
batch carrying the reset and the setup precedes the next request.
The reset happens once, however often reset is called before the
next request. On a MARS connection every open response is canceled
first, as cancelActiveStatement does. Calling it inside
withTransaction is unsupported: the reset rolls
the transaction back, and withTransaction ends with
TransactionEndedEarly.
Unlike sp_reset_connection, the bit leaves the isolation level as it
was on SQL Server (Azure SQL Database puts READ COMMITTED back). To
reset at once, call the procedure: , a round trip that does not re-run the
session setup.callProcedure conn
"sp_reset_connection" []
Encryption
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.
The negotiated protocol state
connectionContext :: Connection -> IO TdsContext Source #
The protocol state negotiated for the connection: the TDS version, the packet size the server granted, whether a transaction is open, the session's collation.
connectionMars :: Connection -> Bool Source #
Whether the connection negotiated MARS (connectMars):
several responses can be open on it at once, and a new request leaves an
open stream alone. For code that is handed a Connection without the
ConnectInfo it was opened with.
data TdsContext Source #
The protocol state a connection has negotiated, which the encoders
and parsers take as an argument; connectionContext
reads a connection's.
Constructors
| TdsContext | |
Fields
| |
Instances
| Show TdsContext Source # | |
Defined in Hmssql.Tds.Context Methods showsPrec :: Int -> TdsContext -> ShowS # show :: TdsContext -> String # showList :: [TdsContext] -> ShowS # | |
| Eq TdsContext Source # | |
Defined in Hmssql.Tds.Context | |
data TdsVersion Source #
Protocol versions this library can negotiate.
TDS 7.4 is the baseline (SQL Server 2017 and later, Azure SQL). TDS 8.0 is the same token stream over TLS from byte zero ("strict" encryption).