clickhouse-haskell-0.1.2.0: src/Database/ClickHouseDriver/Connection.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 CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE BlockArguments #-}
-- | This module contains the implementations of communication with Clickhouse server.
-- Most of functions are for internal use.
-- User should just use Database.ClickHouseDriver.
--
module Database.ClickHouseDriver.Connection
( tcpConnect,
sendQuery,
receiveData,
sendData,
receiveResult,
closeConnection,
processInsertQuery,
ping',
versionTuple,
sendCancel,
)
where
import qualified Database.ClickHouseDriver.Block as Block
import qualified Database.ClickHouseDriver.ClientProtocol as Client
import Database.ClickHouseDriver.Column (ClickhouseType, transpose)
import Database.ClickHouseDriver.Defines
( _BUFFER_SIZE,
_CLIENT_NAME,
_CLIENT_REVISION,
_CLIENT_VERSION_MAJOR,
_CLIENT_VERSION_MINOR,
_DBMS_MIN_REVISION_WITH_CLIENT_INFO,
_DBMS_MIN_REVISION_WITH_QUOTA_KEY_IN_CLIENT_INFO,
_DBMS_MIN_REVISION_WITH_SERVER_DISPLAY_NAME,
_DBMS_MIN_REVISION_WITH_SERVER_TIMEZONE,
_DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES,
_DBMS_MIN_REVISION_WITH_VERSION_PATCH,
_DBMS_NAME,
_DEFAULT_INSERT_BLOCK_SIZE,
_STRINGS_ENCODING,
)
import qualified Database.ClickHouseDriver.Error as Error
import qualified Database.ClickHouseDriver.QueryProcessingStage as Stage
import qualified Database.ClickHouseDriver.ServerProtocol as Server
import Database.ClickHouseDriver.Types
( Block (ColumnOrientedBlock, cdata),
CKResult (CKResult),
ClientInfo (ClientInfo),
ClientSetting
( ClientSetting,
insert_block_size,
strings_as_bytes,
strings_encoding
),
Context (Context, client_info, client_setting, server_info),
Interface (HTTP),
Packet
( Block,
EndOfStream,
ErrorMessage,
Hello,
MultiString,
Progress,
StreamProfileInfo,
queryData
),
QueryInfo,
ServerInfo (..),
TCPConnection (..),
getDefaultClientInfo,
getServerInfo,
readBlockStreamProfileInfo,
readProgress,
storeProfile,
storeProgress,
)
import Database.ClickHouseDriver.IO.BufferedReader
( Buffer (socket),
Reader,
createBuffer,
readBinaryStr,
readVarInt,
refill,
)
import Database.ClickHouseDriver.IO.BufferedWriter
( MonoidMap,
Writer,
writeBinaryStr,
writeVarUInt,
)
import Control.Monad.Loops (iterateWhile)
import Control.Monad.State.Lazy (get, runStateT)
import Control.Monad.Writer (WriterT (runWriterT), execWriterT)
import Control.Monad (when)
import Data.Maybe (fromMaybe)
import Data.ByteString (ByteString)
import Data.Foldable (forM_)
import Data.ByteString.Builder
( Builder,
toLazyByteString,
)
import Data.ByteString.Char8 (unpack)
import qualified Data.List as List (transpose)
import Data.List.Split (chunksOf)
import Data.Vector ((!))
import qualified Data.Vector as V
( concat,
fromList,
length,
map,
)
import qualified Network.Simple.TCP as TCP
( closeSock,
connectSock,
sendLazy,
)
import Network.Socket (Socket)
import System.Timeout (timeout)
--Debug
--import Debug.Trace ( trace )
-- | This module mainly focuses how to make connection
-- | to clickhouse database and protocols to send and receive data
versionTuple :: ServerInfo -> (Word, Word, Word)
versionTuple (ServerInfo _ major minor patch _ _ _) = (major, minor, patch)
-- | internal implementation for ping test.
ping' :: Int
-- ^ Time limit
->TCPConnection
-- ^ host name, port number, and socket are needed
->IO (Maybe String)
-- ^ response `ping`, or nothing indicating server is not properly connected.
ping' timeLimit TCPConnection {tcpHost = host, tcpPort = port, tcpSocket = sock} =
timeout timeLimit $ do
r <- execWriterT $ writeVarUInt Client._PING
TCP.sendLazy sock (toLazyByteString r)
buf <- createBuffer 1024 sock
(packet_type, _) <- runStateT (iterateWhile (== Server._PROGRESS) readVarInt) buf
if packet_type /= Server._PONG
then do
let p_type = Server.toString $ fromIntegral packet_type
let report =
"Unexpected packet from server " <> show host <> ":"
<> show port
<> ", expected "
<> "Pong!"
<> ", got "
<> show p_type
return $
show
Error.ServerException
{ code = Error._UNEXPECTED_PACKET_FROM_SERVER,
message = report,
nested = Nothing
}
else return "PONG!"
-- | send hello has to make for every new connection environment.
sendHello :: (ByteString, ByteString, ByteString)
-- ^ (database name, username, password)
->Socket
-- ^ socket connected to Clickhouse server
->IO ()
sendHello (database, username, password) sock = do
(_, w) <- runWriterT writeHello
TCP.sendLazy sock (toLazyByteString w)
where
writeHello :: Writer Builder
writeHello = do
writeVarUInt Client._HELLO
writeBinaryStr ("ClickHouse " <> _CLIENT_NAME)
writeVarUInt _CLIENT_VERSION_MAJOR
writeVarUInt _CLIENT_VERSION_MINOR
writeVarUInt _CLIENT_REVISION
writeBinaryStr database
writeBinaryStr username
writeBinaryStr password
-- | receive server information if connection is successful, otherwise it would receive error message.
receiveHello :: Buffer
-- ^ Read `Hello` response from server
->IO (Either ByteString ServerInfo)
-- ^ Either error message or server information will be received.
receiveHello buf = do
(res, _) <- runStateT receiveHello' buf
return res
where
receiveHello' :: Reader (Either ByteString ServerInfo)
receiveHello' = do
packet_type <- readVarInt
if packet_type == Server._HELLO
then do
server_name <- readBinaryStr
server_version_major <- readVarInt
server_version_minor <- readVarInt
server_revision <- readVarInt
server_timezone <-
if server_revision >= _DBMS_MIN_REVISION_WITH_SERVER_TIMEZONE
then do Just <$> readBinaryStr
else return Nothing
server_display_name <-
if server_revision >= _DBMS_MIN_REVISION_WITH_SERVER_DISPLAY_NAME
then do
readBinaryStr
else return ""
server_version_dispatch <-
if server_revision >= _DBMS_MIN_REVISION_WITH_VERSION_PATCH
then do
readVarInt
else return server_revision
return $
Right
ServerInfo
{ name = server_name,
version_major = server_version_major,
version_minor = server_version_minor,
version_patch = server_version_dispatch,
revision = server_revision,
timezone = server_timezone,
display_name = server_display_name
}
else
if packet_type == Server._EXCEPTION
then do
e <- readBinaryStr
e2 <- readBinaryStr
e3 <- readBinaryStr
return $ Left ("exception" <> e <> " " <> e2 <> " " <> e3)
else return $ Left "Error"
-- | connect to database through TCP port, used in Client module.
tcpConnect ::
-- | host name to connect
ByteString ->
-- | port name to connect. Default would be 8123
ByteString ->
-- | username. Default would be "default"
ByteString ->
-- | password. Default would be empty string
ByteString ->
-- | database. Default would be "default"
ByteString ->
-- | choose if send and receive data in compressed form. Default would be False.
Bool ->
IO (Either String TCPConnection)
tcpConnect host port user password database compression = do
(sock, sockaddr) <- TCP.connectSock (unpack host) (unpack port)
sendHello (database, user, password) sock
let isCompressed = if compression then Client._COMPRESSION_ENABLE else Client._COMPRESSION_DISABLE
buf <- createBuffer _BUFFER_SIZE sock
hello <- receiveHello buf
case hello of
Right x ->
return $
Right
TCPConnection
{ tcpHost = host,
tcpPort = port,
tcpUsername = user,
tcpPassword = password,
tcpSocket = sock,
tcpSockAdrr = sockaddr,
context =
Context
{ server_info = Just x,
client_info = Nothing,
client_setting =
Just $
ClientSetting
{ insert_block_size = _DEFAULT_INSERT_BLOCK_SIZE,
strings_as_bytes = False,
strings_encoding = _STRINGS_ENCODING
}
},
tcpCompression = isCompressed
}
Left "Exception" -> return $ Left "exception"
Left x -> do
print ("error is " <> x)
TCP.closeSock sock
return $ Left "Connection error"
sendQuery :: TCPConnection
-- ^ To get socket and server info
->ByteString
-- ^ SQL statement
->Maybe ByteString
-- ^ query_id if any
->IO ()
sendQuery TCPConnection {context = Context {server_info = Nothing}} _ _ = error "Empty server info"
sendQuery
TCPConnection
{ tcpCompression = comp,
tcpSocket = sock,
context =
Context
{ server_info = Just info
}
}
query
query_id = do
(_, r) <- runWriterT $ do
writeVarUInt Client._QUERY
writeBinaryStr $ fromMaybe "" query_id
let revision' = revision info
when (revision' >= _DBMS_MIN_REVISION_WITH_CLIENT_INFO)
$ do
let client_info
= getDefaultClientInfo (_DBMS_NAME <> " " <> _CLIENT_NAME)
writeInfo client_info info
writeVarUInt 0 -- TODO add write settings
writeVarUInt Stage._COMPLETE
writeVarUInt comp
writeBinaryStr query
TCP.sendLazy sock (toLazyByteString r)
sendData :: TCPConnection
-- ^ To get socket and context
->ByteString
-- ^ table name
->Maybe Block
-- ^ a block data if any
->IO ()
sendData TCPConnection {tcpSocket = sock, context = ctx} table_name maybe_block = do
-- TODO: ADD REVISION
let info = Block.defaultBlockInfo
r <- execWriterT $ do
writeVarUInt Client._DATA
writeBinaryStr table_name
case maybe_block of
Nothing -> do
Block.writeInfo info
writeVarUInt 0 -- #col
writeVarUInt 0 -- #row
Just block -> do
Block.writeBlockOutputStream ctx block
TCP.sendLazy sock $ toLazyByteString r
-- | Cancel last query sent to server
sendCancel :: TCPConnection -> IO ()
sendCancel TCPConnection {tcpSocket = sock} = do
c <- execWriterT $ writeVarUInt Client._CANCEL
TCP.sendLazy sock (toLazyByteString c)
processInsertQuery ::
-- | source
TCPConnection ->
-- | query without data
ByteString ->
-- | query id
Maybe ByteString ->
-- | data in Haskell type.
[[ClickhouseType]] ->
-- | return 1 if insertion successfully completed
IO ByteString
processInsertQuery
tcp@TCPConnection {tcpSocket = sock, context = Context {client_setting = client_setting}}
query_without_data
query_id
items = do
sendQuery tcp query_without_data query_id
sendData tcp "" Nothing
buf <- createBuffer 2048 sock
let info = case getServerInfo tcp of
Nothing -> error "empty server info"
Just s -> s
(sample_block, _) <-
runStateT
( iterateWhile
( \case
Block {..} -> False
MultiString _ -> True
Hello -> True
x -> error ("unexpected packet type: " ++ show x)
)
$ receivePacket info
)
buf
case client_setting of
Nothing -> error "empty client settings"
Just ClientSetting {insert_block_size = siz} -> do
let chunks = chunksOf (fromIntegral siz) items
mapM_
( \chunk -> do
let vectorized = V.map V.fromList (V.fromList $ List.transpose chunk)
let dataBlock = case sample_block of
Block
typeinfo@ColumnOrientedBlock
{ columns_with_type = cwt
} ->
typeinfo
{ cdata = vectorized
}
x -> error ("unexpected packet type: " ++ show x)
sendData tcp "" (Just dataBlock)
)
chunks
sendData tcp "" Nothing
buf2 <- refill buf
runStateT (receivePacket info) buf2
return "1"
-- | Read data from stream.
receiveData :: ServerInfo -> Reader Block.Block
receiveData info@ServerInfo {revision = revision} = do
_ <-
if revision >= _DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES
then readBinaryStr
else return ""
Block.readBlockInputStream info
-- | Transform received query data into Clickhouse type
receiveResult :: ServerInfo
-- ^ Server information
-> QueryInfo
-- ^ Query information
-> Reader (Either String CKResult)
-- ^ Receive either error message or query result.
receiveResult info query_info = do
packets <- packetGen
let onlyDataPacket = filter isBlock packets
let errors = (\(ErrorMessage str) -> str) <$> filter isError packets
case errors of
[] -> do
let dataVectors = Database.ClickHouseDriver.Column.transpose . Block.cdata . queryData <$> onlyDataPacket
let newQueryInfo = Prelude.foldl updateQueryInfo query_info packets
return $ Right $ CKResult (V.concat dataVectors) newQueryInfo
xs -> do
return $ Left $ Prelude.concat xs
where
updateQueryInfo :: QueryInfo -> Packet -> QueryInfo
updateQueryInfo q (Progress prog) =
storeProgress q prog
updateQueryInfo q (StreamProfileInfo profile) =
storeProfile q profile
updateQueryInfo q _ = q
isError :: Packet -> Bool
isError (ErrorMessage _) = True
isError _ = False
isBlock :: Packet -> Bool
isBlock Block {queryData = Block.ColumnOrientedBlock {cdata = d}} =
V.length d > 0 && V.length (d ! 0) > 0
isBlock _ = False
packetGen :: Reader [Packet]
packetGen = do
packet <- receivePacket info
case packet of
EndOfStream -> return []
error@(ErrorMessage _) -> return [error]
_ -> do
next <- packetGen
return (packet : next)
-- | Receive data packet from server
receivePacket :: ServerInfo -> Reader Packet
receivePacket info = do
packet_type <- readVarInt
-- The pattern matching does not support match with variable name,
-- so here we use number instead.
case packet_type of
1 -> receiveData info >>= (return . Block) -- Data
2 -> Error.readException Nothing >>= (return . ErrorMessage . show) -- Exception
3 -> readProgress (revision info) >>= (return . Progress) -- Progress
5 -> return EndOfStream -- End of Stream
6 -> readBlockStreamProfileInfo >>= (return . StreamProfileInfo) --Profile
7 -> receiveData info >>= (return . Block) -- Total
8 -> receiveData info >>= (return . Block) -- Extreme
-- 10 -> return undefined -- Log
11 -> do
-- MultiStrings message
first <- readBinaryStr
second <- readBinaryStr
return $ MultiString (first, second)
0 -> return Hello -- Hello
_ -> do
closeBufferSocket
error $
show
Error.ServerException
{ code = Error._UNKNOWN_PACKET_FROM_SERVER,
message = "Unknown packet from server",
nested = Nothing
}
closeConnection :: TCPConnection -> IO ()
closeConnection TCPConnection {tcpSocket = sock} = TCP.closeSock sock
-- | write client information and server infomation to protocols
writeInfo ::
(MonoidMap ByteString w) =>
ClientInfo ->
ServerInfo ->
Writer w
writeInfo
( ClientInfo
client_name
interface
client_version_major
client_version_minor
client_version_patch
client_revision
initial_user
initial_query_id
initial_address
quota_key
query_kind
)
ServerInfo {revision = server_revision, display_name = host_name}
| server_revision < _DBMS_MIN_REVISION_WITH_CLIENT_INFO =
error "Method writeInfo is called for unsupported server revision"
| otherwise = do
writeVarUInt 1
writeBinaryStr initial_user
writeBinaryStr initial_query_id
writeBinaryStr initial_address
writeVarUInt (if interface == HTTP then 0 else 1)
writeBinaryStr "" -- os_user. Seems that haskell modules don't have support of getting system username yet.
writeBinaryStr host_name
writeBinaryStr _CLIENT_NAME --client_name
writeVarUInt client_version_major
writeVarUInt client_version_minor
writeVarUInt client_revision
when
(server_revision >= _DBMS_MIN_REVISION_WITH_QUOTA_KEY_IN_CLIENT_INFO)
$ writeBinaryStr quota_key
when
(server_revision >= _DBMS_MIN_REVISION_WITH_VERSION_PATCH)
$ writeVarUInt client_version_patch
-------------------------------------------------------------------------------------------------------------------
---Helpers
{-# INLINE closeBufferSocket #-}
closeBufferSocket :: Reader ()
closeBufferSocket = do
buf <- get
let sock = Database.ClickHouseDriver.IO.BufferedReader.socket buf
forM_ sock TCP.closeSock