clickhouse-haskell-0.1.2.3: src/Database/ClickHouseDriver/HTTP/Client.hs
-- Copyright (c) 2020-present, EMQX, Inc.
-- All rights reserved.
--
-- This source code is distributed under the terms of a MIT license,
-- found in the LICENSE file.
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
-- | This module provides implementation of user's API
module Database.ClickHouseDriver.HTTP.Client
( -- * Setting
setupEnv,
runQuery,
-- * Query
getByteString,
getJSON,
getText,
getTextM,
getJsonM,
insertOneRow,
insertMany,
ping,
exec,
insertFromFile,
-- * Connection
defaultHttpClient,
httpClient,
defaultHttpPool,
)
where
import Control.Concurrent.Async (mapConcurrently)
import Control.Exception (SomeException, try)
import Control.Monad.State.Lazy (MonadIO (..))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.ByteString.Lazy.Builder
( char8,
lazyByteString,
toLazyByteString,
)
import qualified Data.ByteString.Lazy.Char8 as C8
import Data.Default.Class (def)
import Data.Hashable (Hashable (hashWithSalt))
import Data.Pool (Pool, withResource)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Clock (NominalDiffTime)
import Data.Typeable (Typeable)
import Database.ClickHouseDriver.Defines as Defines
( _DEFAULT_HOST,
_DEFAULT_HTTP_PORT,
)
import Database.ClickHouseDriver.HTTP.Connection
( createHttpPool,
defaultHttpConnection,
httpConnectDb,
)
import Database.ClickHouseDriver.HTTP.Helpers
( extract,
genURL,
toString,
)
import Database.ClickHouseDriver.HTTP.Types (Format (..), HttpConnection (..), JSONResult)
import Database.ClickHouseDriver.Types (ClickhouseType)
import Haxl.Core
( BlockedFetch (..),
DataSource (fetch),
DataSourceName (..),
Env (userEnv),
GenHaxl,
PerformFetch (SyncFetch),
ShowP (..),
StateKey (State),
dataFetch,
initEnv,
putFailure,
putSuccess,
runHaxl,
stateEmpty,
stateSet,
)
import Network.HTTP.Client
( RequestBody (..),
httpLbs,
method,
parseRequest,
requestBody,
responseBody,
streamFile,
)
import Text.Printf (printf)
{-Implementation in Haxl-}
--
data HttpClient a where
FetchByteString :: String -> HttpClient BS.ByteString
FetchJSON :: String -> HttpClient BS.ByteString
FetchCSV :: String -> HttpClient BS.ByteString
FetchText :: String -> HttpClient BS.ByteString
Ping :: HttpClient BS.ByteString
deriving instance Show (HttpClient a)
deriving instance Typeable HttpClient
deriving instance Eq (HttpClient a)
instance ShowP HttpClient where showp = show
instance Hashable (HttpClient a) where
hashWithSalt salt (FetchByteString cmd) = hashWithSalt salt cmd
hashWithSalt salt (FetchJSON cmd) = hashWithSalt salt cmd
hashWithSalt salt (FetchCSV cmd) = hashWithSalt salt cmd
hashWithSalt salt Ping = hashWithSalt salt ("ok" :: BS.ByteString)
instance DataSourceName HttpClient where
dataSourceName _ = "ClickhouseDataSource"
instance DataSource u HttpClient where
fetch (settings) _flags _usrenv = SyncFetch $ \blockedFetches -> do
printf "Fetching %d queries.\n" (length blockedFetches)
res <- mapConcurrently (fetchData settings) blockedFetches
case res of
[()] -> return ()
instance StateKey HttpClient where
data State HttpClient
= SingleHttp HttpConnection
| HttpPool (Pool HttpConnection)
class HttpEnvironment a where
toEnv :: a -> State HttpClient
pick :: a -> IO HttpConnection
instance HttpEnvironment HttpConnection where
toEnv = SingleHttp
pick = return
instance HttpEnvironment (Pool HttpConnection) where
toEnv = HttpPool
pick pool = withResource pool $ return
-- | fetch function
fetchData ::
State HttpClient -> --Connection configuration
BlockedFetch HttpClient -> --fetched data
IO ()
fetchData (settings) fetches = do
let (queryWithType, var) = case fetches of
BlockedFetch (FetchJSON query) var' -> (query ++ " FORMAT JSON", var')
BlockedFetch (FetchCSV query) var' -> (query ++ " FORMAT CSV", var')
BlockedFetch (FetchByteString query) var' -> (query, var')
BlockedFetch Ping var' -> ("ping", var')
e <- Control.Exception.try $ do
case settings of
SingleHttp http@(HttpConnection _ mng) -> do
url <- genURL http queryWithType
req <- parseRequest url
ans <- responseBody <$> httpLbs req mng
return $ LBS.toStrict ans
HttpPool pool ->
withResource pool $ \conn@(HttpConnection _ mng) -> do
url <- genURL conn queryWithType
req <- parseRequest url
ans <- responseBody <$> httpLbs req mng
return $ LBS.toStrict ans
either
(putFailure var)
(putSuccess var)
(e :: Either SomeException (BS.ByteString))
-- | Fetch data from ClickHouse client in the text format.
getByteString :: String -> GenHaxl u w BS.ByteString
getByteString = dataFetch . FetchByteString
getText :: String -> GenHaxl u w T.Text
getText cmd = fmap decodeUtf8 (getByteString cmd)
-- | Fetch data from ClickHouse client in the JSON format.
getJSON :: String -> GenHaxl u w JSONResult
getJSON cmd = fmap extract (dataFetch $ FetchJSON cmd)
-- | Fetch data from Clickhouse client with commands warped in a Traversable monad.
getTextM :: (Monad m, Traversable m) => m String -> GenHaxl u w (m T.Text)
getTextM = mapM getText
-- | Fetch data from Clickhouse client in the format of JSON
getJsonM :: (Monad m, Traversable m) => m String -> GenHaxl u w (m JSONResult)
getJsonM = mapM getJSON
-- | actual function used by user to perform fetching command
exec :: (HttpEnvironment a) => String -> Env a w -> IO (Either C8.ByteString String)
exec cmd' env = do
let cmd = C8.pack cmd'
conn@HttpConnection {httpManager = mng} <- pick $ userEnv env
url <- genURL conn ""
req <- parseRequest url
ans <-
responseBody
<$> httpLbs
req
{ method = "POST",
requestBody = RequestBodyLBS cmd
}
mng
if ans /= ""
then return $ Left ans -- error message
else return $ Right "Created successfully"
-- | insert one row
insertOneRow ::
(HttpEnvironment a) =>
String ->
[ClickhouseType] ->
Env a w ->
IO (Either C8.ByteString String)
insertOneRow table_name arr environment = do
let row = toString arr
let cmd = C8.pack ("INSERT INTO " ++ table_name ++ " VALUES " ++ row)
settings@HttpConnection {httpManager = mng} <- pick $ userEnv environment
url <- genURL settings ""
req <- parseRequest url
ans <-
responseBody
<$> httpLbs
req
{ method = "POST",
requestBody = RequestBodyLBS cmd
}
mng
if ans /= ""
then return $ Left ans -- error messagethe hellenic republic
else return $ Right "Inserted successfully"
-- | insert one or more rows
insertMany ::
(HttpEnvironment a) =>
String ->
[[ClickhouseType]] ->
Env a w ->
IO (Either C8.ByteString String)
insertMany table_name rows environment = do
let rowsString = map (lazyByteString . C8.pack . toString) rows
comma = char8 ','
preset = lazyByteString $ C8.pack $ "INSERT INTO " <> table_name <> " VALUES "
togo = preset <> (foldl1 (\x y -> x <> comma <> y) rowsString)
settings@HttpConnection {httpManager = mng} <- pick $ userEnv environment
url <- genURL settings ""
req <- parseRequest url
ans <-
responseBody
<$> httpLbs
req
{ method = "POST",
requestBody = RequestBodyLBS $ toLazyByteString togo
}
mng
print "inserted successfully"
if ans /= ""
then return $ Left ans
else return $ Right "Successful insertion"
-- | insert data from
insertFromFile ::
(HttpEnvironment a) =>
String ->
Format ->
FilePath ->
Env a w ->
IO (Either C8.ByteString String)
insertFromFile table_name format file environment = do
fileReqBody <- streamFile file
settings@HttpConnection {httpManager = mng} <- pick $ userEnv environment
url <-
genURL
settings
( "INSERT INTO " <> table_name
<> case format of
CSV -> " FORMAT CSV"
JSON -> " FORMAT JSON"
TUPLE -> " VALUES"
)
req <- parseRequest url
ans <-
responseBody
<$> httpLbs
req
{ method = "POST",
requestBody = fileReqBody
}
mng
if ans /= ""
then return $ Left ans -- error message
else return $ Right "Inserted successfully"
ping :: GenHaxl u w BS.ByteString
ping = dataFetch $ Ping
-- | Default environment
setupEnv :: (MonadIO m, HttpEnvironment a) => a -> m (Env a w)
setupEnv csetting = liftIO $ initEnv (stateSet (toEnv csetting) stateEmpty) csetting
defaultHttpClient :: (MonadIO m) => m (Env HttpConnection w)
defaultHttpClient = liftIO $ defaultHttpConnection >>= setupEnv
defaultHttpPool :: (MonadIO m) => Int -> NominalDiffTime -> Int -> m (Env (Pool HttpConnection) w)
defaultHttpPool numStripes idleTime maxResources =
liftIO $ createHttpPool def numStripes idleTime maxResources >>= setupEnv
httpClient :: (MonadIO m) => String -> String -> m (Env HttpConnection w)
httpClient user password = liftIO $ httpConnectDb user password Defines._DEFAULT_HTTP_PORT Defines._DEFAULT_HOST Nothing >>= setupEnv
-- | rename runHaxl function.
{-# INLINE runQuery #-}
runQuery :: (MonadIO m) => Env u w -> GenHaxl u w a -> m a
runQuery env haxl = liftIO $ runHaxl env haxl