diff --git a/cabal.project b/cabal.project index a3be0b10..e1265f94 100644 --- a/cabal.project +++ b/cabal.project @@ -2,6 +2,7 @@ packages: ./library ./ecosystem/tls ./ecosystem/eventlog2ch + ./ecosystem/PoC-clickhouse-proxy ./contribution ./contribution/modules/protocol-docs ./contribution/modules/visits diff --git a/ecosystem/PoC-clickhouse-proxy/ClickHouseProxy/TcpServer.hs b/ecosystem/PoC-clickhouse-proxy/ClickHouseProxy/TcpServer.hs new file mode 100644 index 00000000..b4e44045 --- /dev/null +++ b/ecosystem/PoC-clickhouse-proxy/ClickHouseProxy/TcpServer.hs @@ -0,0 +1,88 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE OverloadedRecordDot #-} +module ClickHouseProxy.TcpServer ( + runTcpServer +) where + +import ClickHaskell (ToChType (..)) +import ClickHaskell.Internal +import ClickHaskell.Protocol (ClientPacket (..), ExceptionPacket (..), HelloPacket (..), Serializable (..), ServerPacket (..)) +import Control.Concurrent (forkFinally) +import Control.Exception as E (bracket, bracketOnError) +import Control.Monad (forever, void) +import Data.Binary (Get) +import Data.Binary.Get (Decoder (..), runGetIncremental) +import Data.ByteString as BS (ByteString) +import Data.ByteString.Char8 as BS (putStrLn) +import Data.ByteString.Char8 as BS8 (pack) +import Data.List.NonEmpty as NE (head) +import Network.Socket + + +runTcpServer :: IO () +runTcpServer = + runTCPServer Nothing "9000" \sock -> do + BS.putStrLn "Opening connection" + buffer <- mkBuffer =<< defaultMkBufferArgs "" sock + talk buffer + where + talk buffer = do + packet <- readBuffer buffer (deserialize @ClientPacket 54459) + case packet of + Hello hello -> do + Prelude.putStrLn $ + "client: " <> show hello.client_name <> " " <> + "client_version_major: " <> show hello.client_version_major <> " " <> + "client_version_minor: " <> show hello.client_version_minor <> " " <> + "tcp_protocol_version: " <> "54459" <> " " <> + "default_database: " <> show hello.default_database <> " " <> + "user: " <> show hello.user <> " " <> + "pass: " <> show hello.pass <> " " <> + "" + writeConn buffer + . serialize 54459 + $ Exception MkExceptionPacket{ + code = 0, + name = toChType @_ @ByteString $ BS8.pack "Test exception", + message = toChType @_ @ByteString $ BS8.pack "\"Hello, World!\"", + stack_trace = toChType @_ @ByteString $ BS8.pack "", + nested = 0 + } + _other -> error "Unexpected packet" + + +runTCPServer :: Maybe HostName -> ServiceName -> (Socket -> IO a) -> IO a +runTCPServer mhost port server = do + addr <- resolve + E.bracket (open addr) close loop + where + resolve = do + let hints = defaultHints { + addrFlags = [AI_PASSIVE], + addrSocketType = Stream + } + NE.head <$> getAddrInfo (Just hints) mhost (Just port) + open addr = + E.bracketOnError (openSocket addr) close $ \sock -> do + setSocketOption sock ReuseAddr 1 + withFdSocket sock setCloseOnExecIfNeeded + bind sock $ addrAddress addr + listen sock 1024 + return sock + loop sock = + forever $ E.bracketOnError (accept sock) (close . fst) + $ \(conn, _peer) -> void $ + forkFinally (server conn) (const $ gracefulClose conn 5000) + + +readBuffer :: Buffer -> Get a -> IO a +readBuffer MkBuffer{readBuff, writeBuff} parser = runBufferReader (runGetIncremental parser) + where + runBufferReader :: Decoder packet -> IO packet + runBufferReader dec = case dec of + (Partial decoder) -> readBuff >>= runBufferReader . decoder . Just + (Done leftover _consumed packet) -> packet <$ writeBuff leftover + (Fail _leftover _consumed msg) -> error ("Deserialization error: " <> msg) diff --git a/ecosystem/PoC-clickhouse-proxy/LICENSE b/ecosystem/PoC-clickhouse-proxy/LICENSE new file mode 100644 index 00000000..203b8458 --- /dev/null +++ b/ecosystem/PoC-clickhouse-proxy/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, Dmitry Kovalev + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ecosystem/PoC-clickhouse-proxy/README.md b/ecosystem/PoC-clickhouse-proxy/README.md new file mode 100644 index 00000000..4975e45d --- /dev/null +++ b/ecosystem/PoC-clickhouse-proxy/README.md @@ -0,0 +1 @@ +# clickhouse-proxy diff --git a/ecosystem/PoC-clickhouse-proxy/clickhouse-proxy.cabal b/ecosystem/PoC-clickhouse-proxy/clickhouse-proxy.cabal new file mode 100644 index 00000000..8d6e1d7d --- /dev/null +++ b/ecosystem/PoC-clickhouse-proxy/clickhouse-proxy.cabal @@ -0,0 +1,43 @@ +cabal-version: 3.0 + +name: clickhouse-proxy +version: 0.1 +author: Dmitry Kovalev +maintainer: Dmitry Kovalev +category: ClickHouse +synopsis: ClickHouse proxy +description: ClickHouse TCP protocol proxy +homepage: https://github.com/KovalevDima/clickhouse-proxy +bug-reports: https://github.com/KovalevDima/clickhouse-proxy +license: BSD-3-Clause +license-file: LICENSE +copyright: 2026 Dmitry Kovalev +build-type: Simple + +extra-doc-files: + README.md + +source-repository head + type: git + location: https://github.com/KovalevDima/clickhouse-proxy + +executable clickhouse-proxy + main-is: clickhouse-proxy.hs + hs-source-dirs: . + ghc-options: + -Wall + -Wunused-packages + build-depends: + -- Internal dependencies + ClickHaskell, + -- GHC included libraries + base >= 4.7 && <5, + bytestring, + binary, + -- External dependencies + network < 4, + other-modules: + ClickHouseProxy.TcpServer + default-language: Haskell2010 + default-extensions: + BlockArguments diff --git a/ecosystem/PoC-clickhouse-proxy/clickhouse-proxy.hs b/ecosystem/PoC-clickhouse-proxy/clickhouse-proxy.hs new file mode 100644 index 00000000..d54c3e83 --- /dev/null +++ b/ecosystem/PoC-clickhouse-proxy/clickhouse-proxy.hs @@ -0,0 +1,8 @@ +module Main where + +import ClickHouseProxy.TcpServer (runTcpServer) + +main :: IO () +main = do + putStrLn "Starting server" + runTcpServer diff --git a/library/ClickHaskell.cabal b/library/ClickHaskell.cabal index b7d82280..5e85e184 100644 --- a/library/ClickHaskell.cabal +++ b/library/ClickHaskell.cabal @@ -34,6 +34,7 @@ Library Exposed-Modules: ClickHaskell ClickHaskell.Protocol + ClickHaskell.Internal Other-Modules: ClickHaskell.Connection ClickHaskell.Protocol.Client diff --git a/library/ClickHaskell/Internal.hs b/library/ClickHaskell/Internal.hs new file mode 100644 index 00000000..b3545200 --- /dev/null +++ b/library/ClickHaskell/Internal.hs @@ -0,0 +1,8 @@ +module ClickHaskell.Internal ( + Buffer(..), + mkBuffer, + defaultMkBufferArgs, +) where + +-- Internal +import ClickHaskell.Connection