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