hmssql
Safe HaskellNone
LanguageHaskell2010

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

These 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 plain queryS stream stays open until it is read to its end or until cancelResponse, cancelActiveStatement or release cancels 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_SNAPSHOT on 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 in connectSessionSetup, 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 in connectSessionSetup. SQL Server undoes a change made beside a running stream when that stream ends, which includes the isolation level and XACT_ABORT. withTransactionLevel, #temp tables and SESSION_CONTEXT are 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

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

  • connectHost :: !Text

    A host name or an IP address. A name that resolves to several addresses is tried in the resolver's order until one accepts. An address that hasn't answered within 4 seconds is given up and the next is tried; the last one waits as long as the operating system allows. Whatever the protocol, this is also the name the server's certificate is checked against.

  • connectPort :: !Text

    A port number, or a service name. Used over TCP only.

  • connectProtocol :: !Protocol

    What the connection travels on. The default, ProtocolAuto, is TCP everywhere but on Windows, where it does what Microsoft's clients do, so that a SQL Server on the same machine is reached even with TCP/IP turned off, as it is after a default installation of some editions.

  • connectInstance :: !Text

    The name of a named instance, for ProtocolSharedMemory and ProtocolNamedPipes, which find an instance by its name; empty for the default instance. TCP ignores it: a named instance is reached by its port (hmssql doesn't ask the SQL Server Browser).

  • connectUser :: !Text

    The SQL Server login. (Windows authentication is not supported; Entra ID is, through connectAccessToken.)

  • connectPassword :: !Text
     
  • connectAccessToken :: !(Maybe Text)

    An Entra ID access token, used in place of connectUser and connectPassword, which must then be empty, or connect throws EncodeError. Obtaining the token is the caller's job. It is what Azure Managed Identity's endpoint, a service principal's credentials or az account get-access-token return for the audience https://database.windows.net/, passed as it comes: hmssql sends it in the login, as SqlClient's AccessToken does, and never parses it.

    The server checks the token at login only, so a connection outlives its token's expiry. A pool should get a fresh token before each connect; the managed identity endpoint caches them. With EncryptOff or EncryptNotSup the token would travel in plaintext, like a password, but Azure SQL requires encryption anyway.

  • connectDatabase :: !Text

    The database to open in; empty for the login's default.

  • connectEncryption :: !Encryption

    The default, EncryptOn, encrypts the whole session and refuses a server that can't. EncryptStrict selects TDS 8.0, where TLS starts before the first byte of TDS; the other modes negotiate TLS in PRELOGIN, under TDS 7.4.

  • connectTls :: !TlsSettings

    How the server's certificate is validated: against the system store by default. A local server with a self-signed certificate needs tlsTrustServerCertificate, or its certificate in tlsCaCertificateFiles.

  • connectApplicationName :: !Text

    What the session shows as program_name in sys.dm_exec_sessions.

  • connectServerName :: !Text

    The server name sent in the login. Empty, the default, sends connectHost as it is, a fully qualified Azure SQL host name included. Set it when a tunnel or an alias gives the server a different network address from its logical name. A login redirect replaces it with the destination the server names.

  • connectLanguage :: !Text

    The session's language (SET LANGUAGE), which sets the date format and the language of the server's messages; empty for the login's default.

  • connectPacketSize :: !Word32

    The packet size to ask for, header included: 8000 by default, as SqlClient's. Values outside 512 to 32767 are clamped to that range. The server may grant less than asked, and connectionContext has the negotiated size. With connectMars over TLS, at most 16368 is asked for, because a packet and the 16 bytes MARS adds to it must fit in one TLS record.

  • connectReadOnlyIntent :: !Bool

    Declare the connection read-only in the login (what ApplicationIntent=ReadOnly does in other drivers), so that an Availability Group listener routes it to a readable secondary. A standalone server ignores it.

  • connectSessionSetup :: ![Text]

    Statements run after login, joined with newlines into one batch, and run again as one batch after each reset, which includes each release. With an empty list, reset costs no round trip of its own. See defaultSessionSetup for what the login already sets.

  • connectMars :: !Bool

    Multiple Active Result Sets: several responses can be open on the connection at once, each request on a session of its own, as SqlClient's MultipleActiveResultSets=True does. Off by default, so a new request abandons a response that is still open (ResultAbandoned). Hmssql.Connection has the rules. connect throws MarsNotSupported if the server doesn't support MARS.

  • connectMarsWindow :: !Int

    With connectMars: how many packets the server may send ahead of what has been read, on each open response; at least 4. It bounds what a response nobody is reading keeps in memory (the window times the packet size) and, over a long round trip, how fast one stream can be read. The default, 64, kept up with a plain connection over a 45 ms round trip.

  • connectPinReader :: !Bool

    Pin the connection's reader thread to the capability of the thread that calls connect, instead of leaving it unbound (the default). When that thread is the one that makes the requests, a local round trip with +RTS -N on many cores is 30 to 40% faster; but every connection a pool opens from one thread then has its reader on that one capability.

  • connectValueAllocAhead :: !Int

    How much buffer a large value, such as a row longer than 64 KiB or an output parameter, may be allocated before its bytes arrive. A value declared at most this long gets its whole buffer as it starts arriving, and a longer one grows as it arrives. The limit bounds what a server that declares a value it doesn't send can make the client allocate. 1 MiB by default, and 0 allocates nothing ahead.

  • connectOnInfoMessage :: !(ServerMessage -> IO ())

    Called for every INFO token (PRINT output, low-severity messages), on the thread reading the response, while it holds the connection. It must not use the connection (that throws ReentrantCallback) or wait for another thread that does (that deadlocks). An exception it throws propagates to the reader; the connection stays usable.

defaultConnectInfo :: ConnectInfo Source #

The defaults, which the example at the top of Hmssql starts from:

  • localhost on port 1433, over ProtocolAuto, to the default instance.
  • No user, password, access token or database.
  • EncryptOn with defaultTlsSettings: 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.

data Protocol Source #

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 connectHost is this machine (localhost, ., (local) or the machine's name), then TCP, then Named Pipes, the order Microsoft's clients use. The next is tried only when one can't be opened; a server that was reached and then refused the TLS handshake or the login is not tried another way. Elsewhere: TCP.

A connectPort other than the default, 1433, means TCP and nothing else, on Windows too: the port names one server, and a pipe might lead to another. With a connectInstance and the default port, only the pipes are tried, since TCP would need the instance's port.

ProtocolTcp

TCP, to connectHost and connectPort.

ProtocolSharedMemory

Shared Memory: a SQL Server on this machine, whatever connectHost says.

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 connect closes it when the login fails or the connection is closed. It runs with asynchronous exceptions masked, and should close what it opened if it is interrupted. A login redirect is still followed over TCP.

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 #

Open a connection with connect, run the action with it, and close it afterward, whether the action returns or throws.

connect :: ConnectInfo -> IO Connection Source #

Open a connection and log in.

If the server can't be reached, this throws ConnectionClosedBy (TransportFailed e) with the underlying 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 ConnectionClosedBy ClosedByClient, as does any later use of the connection.

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 discard

This 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 cancelActiveStatement does, 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 release is 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: callProcedure conn "sp_reset_connection" [], a round trip that does not re-run the session setup.

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 EncryptOn does. (0x03)

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

Instances details
Show Encryption Source # 
Instance details

Defined in Hmssql.Tds.Client

Eq Encryption Source # 
Instance details

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

  • tlsTrustServerCertificate :: !Bool

    Skip certificate validation entirely. Encryption still applies, but the server's identity is not verified.

    A certificate whose extended key usage extension doesn't include server authentication is refused even so: tls checks that itself, whatever the validation decided. (SQL Server doesn't use such a certificate anyway: it requires a certificate for server authentication.) With tls 2.5 or later, so is a certificate whose RSA key is shorter than 2048 bits.

  • tlsUseSystemCertificateStore :: !Bool

    Trust the system's certificate store. It is loaded once per process, by the first connection that uses it (loading it takes about 20 ms and 38 MB of allocation on macOS, where it runs the security tool), so certificates added to the system store later are seen only by a new process.

  • tlsCaCertificateFiles :: ![FilePath]

    PEM files whose certificates are trusted in addition to the system store. A self-signed server certificate can be listed here directly. Each is read and parsed by every connection that uses it (about 70 us for a one-certificate file), so a replaced file takes effect with the next connection; an application that connects often and wants to parse a file once can read it itself (readSignedObject) and pass the certificates as tlsExtraCertificates.

  • tlsExtraCertificates :: ![SignedCertificate]

    Trusted certificates supplied in memory.

  • tlsHostNameInCertificate :: !(Maybe Text)

    Validate the certificate against this name instead of the host that was connected to. The server name sent in the TLS handshake (SNI) is still connectHost, or none if that is an IP address.

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

Instances details
Show TdsContext Source # 
Instance details

Defined in Hmssql.Tds.Context

Eq TdsContext Source # 
Instance details

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

Constructors

Tds74 
Tds80