cql-io-1.0.1: src/Database/CQL/IO.hs
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-- | This driver operates on some state which must be initialised prior to
-- executing client operations and terminated eventually. The library uses
-- <http://hackage.haskell.org/package/tinylog tinylog> for its logging
-- output and expects a 'Logger'.
--
-- For example (here using the @OverloadedStrings@ extension) :
--
-- @
-- > import Data.Text (Text)
-- > import Data.Functor.Identity
-- > import Database.CQL.IO as Client
-- > import qualified System.Logger as Logger
-- >
-- > g <- Logger.new Logger.defSettings
-- > c <- Client.init g defSettings
-- > let q = "SELECT cql_version from system.local" :: QueryString R () (Identity Text)
-- > let p = defQueryParams One ()
-- > runClient c (query q p)
-- [Identity "3.4.4"]
-- > shutdown c
-- @
--
--
-- __Note on prepared statements__
--
-- Prepared statements are fully supported but imply certain
-- complexities which lead to some assumptions beyond the scope
-- of the CQL binary protocol specification (spec):
--
-- (1) The spec scopes the 'QueryId' to the node the query has
-- been prepared with. The spec does not state anything
-- about the format of the 'QueryId', however it seems that
-- at least the official Java driver assumes that any given
-- 'QueryString' yields the same 'QueryId' on every node.
-- We make the same assumption.
-- (2) In case a node does not know a given 'QueryId' an 'Unprepared'
-- error is returned. We assume that it is always safe to then
-- transparently re-prepare the corresponding 'QueryString' and
-- to re-execute the original request against the same node.
--
-- Besides these assumptions there is also a potential tradeoff in
-- regards to /eager/ vs. /lazy/ query preparation.
-- We understand /eager/ to mean preparation against all current nodes of
-- a cluster and /lazy/ to mean preparation against a single node if
-- required, i.e. after an 'Unprepared' error response. Which strategy to
-- choose depends on the scope of query reuse and the size of the cluster.
-- The global default can be changed through the 'Settings' module and per
-- action using 'withPrepareStrategy'.
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE LambdaCase #-}
module Database.CQL.IO
( -- * Client Settings
Settings
, S.defSettings
, addContact
, setCompression
, setConnectTimeout
, setContacts
, setIdleTimeout
, setKeyspace
, setMaxConnections
, setMaxStreams
, setMaxTimeouts
, setPolicy
, setPoolStripes
, setPortNumber
, PrepareStrategy (..)
, setPrepareStrategy
, setProtocolVersion
, setResponseTimeout
, setSendTimeout
, setRetrySettings
, setMaxRecvBuffer
, setSSLContext
-- ** Authentication
, setAuthentication
, Authenticator (..)
, AuthContext
, ConnId
, authConnId
, authHost
, AuthMechanism (..)
, AuthUser (..)
, AuthPass (..)
, passwordAuthenticator
-- ** Retry Settings
, RetrySettings
, noRetry
, retryForever
, maxRetries
, adjustConsistency
, constDelay
, expBackoff
, fibBackoff
, adjustSendTimeout
, adjustResponseTimeout
-- ** Load-balancing
, Policy (..)
, random
, roundRobin
-- *** Hosts
, Host
, HostEvent (..)
, InetAddr (..)
, hostAddr
, dataCentre
, rack
-- * Client Monad
, Client
, MonadClient (..)
, ClientState
, DebugInfo (..)
, init
, runClient
, shutdown
, debugInfo
-- * Queries
, R, W, S
, QueryParams (..)
, defQueryParams
, Consistency (..)
, SerialConsistency (..)
, QueryString (..)
-- ** Basic Queries
, query
, query1
, write
, schema
-- ** Prepared Queries
, PrepQuery
, prepared
, queryString
-- ** Paging
, Page (..)
, emptyPage
, paginate
-- ** Lightweight Transactions
, Row
, fromRow
, trans
-- ** Batch Queries
, BatchM
, addQuery
, addPrepQuery
, setType
, setConsistency
, setSerialConsistency
, batch
-- ** Retries
, retry
, once
-- ** Low-Level Queries
--
-- | Note: Use of these low-level functions may require additional imports from
-- @Database.CQL.Protocol@ or its submodules in order to construct
-- 'Request's and evaluate 'Response's.
, RunQ (..)
, request
-- * Exceptions
, InvalidSettings (..)
, InternalError (..)
, HostError (..)
, ConnectionError (..)
, UnexpectedResponse (..)
, Timeout (..)
, HashCollision (..)
, AuthenticationError (..)
) where
import Control.Applicative
import Control.Monad.Catch
import Data.Maybe (isJust, listToMaybe)
import Database.CQL.Protocol
import Database.CQL.IO.Batch hiding (batch)
import Database.CQL.IO.Client
import Database.CQL.IO.Cluster.Host
import Database.CQL.IO.Cluster.Policies
import Database.CQL.IO.Connection.Settings as C
import Database.CQL.IO.PrepQuery
import Database.CQL.IO.Settings as S
import Database.CQL.IO.Types
import Prelude hiding (init)
import qualified Database.CQL.IO.Batch as B
-- | A type which can be run as a query.
class RunQ q where
runQ :: (MonadClient m, Tuple a, Tuple b)
=> q k a b
-> QueryParams a
-> m (Response k a b)
instance RunQ QueryString where
runQ q p = request (RqQuery (Query q p))
instance RunQ PrepQuery where
runQ q = liftClient . execute q
-- | Construct default 'QueryParams' for the given consistency
-- and bound values. In particular, no page size, paging state
-- or serial consistency will be set.
defQueryParams :: Consistency -> a -> QueryParams a
defQueryParams c a = QueryParams
{ consistency = c
, values = a
, skipMetaData = False
, pageSize = Nothing
, queryPagingState = Nothing
, serialConsistency = Nothing
, enableTracing = Nothing
}
-- | Run a CQL read-only query returning a list of results.
query :: (MonadClient m, Tuple a, Tuple b, RunQ q) => q R a b -> QueryParams a -> m [b]
query q p = do
r <- runQ q p
getResult r >>= \case
RowsResult _ b -> return b
_ -> throwM $ UnexpectedResponse r
-- | Run a CQL read-only query returning a single result.
query1 :: (MonadClient m, Tuple a, Tuple b, RunQ q) => q R a b -> QueryParams a -> m (Maybe b)
query1 q p = listToMaybe <$> query q p
-- | Run a CQL write-only query (e.g. insert\/update\/delete),
-- returning no result.
--
-- /Note: If the write operation is conditional, i.e. is in fact a "lightweight
-- transaction" returning a result, 'trans' must be used instead./
write :: (MonadClient m, Tuple a, RunQ q) => q W a () -> QueryParams a -> m ()
write q p = do
r <- runQ q p
getResult r >>= \case
VoidResult -> return ()
_ -> throwM $ UnexpectedResponse r
-- | Run a CQL conditional write query (e.g. insert\/update\/delete) as a
-- "lightweight transaction", returning the result 'Row's describing the
-- outcome.
trans :: (MonadClient m, Tuple a, RunQ q) => q W a Row -> QueryParams a -> m [Row]
trans q p = do
r <- runQ q p
getResult r >>= \case
RowsResult _ b -> return b
_ -> throwM $ UnexpectedResponse r
-- | Run a CQL schema query, returning 'SchemaChange' information, if any.
schema :: (MonadClient m, Tuple a, RunQ q) => q S a () -> QueryParams a -> m (Maybe SchemaChange)
schema q p = do
r <- runQ q p
getResult r >>= \case
SchemaChangeResult s -> return $ Just s
VoidResult -> return Nothing
_ -> throwM $ UnexpectedResponse r
-- | Run a batch query against a Cassandra node.
batch :: MonadClient m => BatchM () -> m ()
batch = liftClient . B.batch
-- | Return value of 'paginate'. Contains the actual result values as well
-- as an indication of whether there is more data available and the actual
-- action to fetch the next page.
data Page a = Page
{ hasMore :: !Bool
, result :: [a]
, nextPage :: Client (Page a)
} deriving (Functor)
-- | A page with an empty result list.
emptyPage :: Page a
emptyPage = Page False [] (return emptyPage)
-- | Run a CQL read-only query against a Cassandra node.
--
-- This function is like 'query', but limits the result size to 10000
-- (default) unless there is an explicit size restriction given in
-- 'QueryParams'. The returned 'Page' can be used to continue the query.
--
-- Please note that -- as of Cassandra 2.1.0 -- if your requested page size
-- is equal to the result size, 'hasMore' might be true and a subsequent
-- 'nextPage' will return an empty list in 'result'.
paginate :: (MonadClient m, Tuple a, Tuple b, RunQ q) => q R a b -> QueryParams a -> m (Page b)
paginate q p = do
let p' = p { pageSize = pageSize p <|> Just 10000 }
r <- runQ q p'
getResult r >>= \case
RowsResult m b ->
if isJust (pagingState m) then
return $ Page True b (paginate q p' { queryPagingState = pagingState m })
else
return $ Page False b (return emptyPage)
_ -> throwM $ UnexpectedResponse r