Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

on:
pull_request:
push:
branches:
- main
- master

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up GHC and Cabal
uses: haskell-actions/setup@v2
with:
ghc-version: '9.10.3'
cabal-version: '3.16'

- name: Cache Cabal artifacts
uses: actions/cache@v4
with:
path: |
~/.cabal/packages
~/.cabal/store
dist-newstyle
key: ${{ runner.os }}-ghc-9.10.3-cabal-${{ hashFiles('**/*.cabal', 'cabal.project', 'cabal.project.local') }}
restore-keys: |
${{ runner.os }}-ghc-9.10.3-cabal-

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y pkg-config zlib1g-dev

- name: Run tests
run: |
cabal update
cabal test all --test-show-details=direct
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,50 @@ let ins = Insert.insert "events" ["id", "name"] rowEncoder mempty
runInsert connection ins () [(1, "signup"), (2, "purchase")]
```

For richer row types, the pattern is the same: define a record for one row, then compose a row encoder by projecting each field into the appropriate `Value`.

```haskell
import Data.Functor.Contravariant (contramap)
import Data.HashMap.Strict qualified as HashMap
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Word (Word64)
import Database.ClickHouse
import Database.ClickHouse.Insert qualified as Insert
import Database.ClickHouse.Value qualified as Value

data EventRow = EventRow
{ eventId :: Word64
, eventName :: Text
, userId :: Maybe Word64
, tags :: [Text]
, attributes :: HashMap.HashMap Text Text
, createdAt :: UTCTime
}

let rowEncoder =
contramap eventId Value.uint64
<> contramap eventName Value.string
<> contramap userId (Value.nullable Value.uint64)
<> contramap tags (Value.array Value.string)
<> contramap attributes (Value.map Value.string Value.string)
<> contramap createdAt Value.dateTime

let ins =
Insert.insert
"events"
["event_id", "event_name", "user_id", "tags", "attributes", "created_at"]
rowEncoder
mempty
```

That composes one encoder for the whole row out of per-column encoders. See `examples/ComplexInsert.hs` for a full runnable example.

## Examples

- `examples/SimpleQuery.hs`
- `examples/SimpleInsert.hs`
- `examples/ComplexInsert.hs`

## Supported value shapes

Expand Down
2 changes: 2 additions & 0 deletions clickhouse-client.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ test-suite clickhouse-client-test
, base >=4.19 && <4.21
, bytestring
, clickhouse-client
, hashable
, tasty
, tasty-hunit
, text
, time
, unordered-containers
, uuid
71 changes: 71 additions & 0 deletions examples/ComplexInsert.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Data.Functor.Contravariant (contramap)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Word (Word64)
import Database.ClickHouse
import Database.ClickHouse.Insert qualified as Insert
import Database.ClickHouse.Value qualified as Value

-- A more realistic row type than a 2-tuple.
data EventRow = EventRow
{ eventId :: Word64,
eventName :: Text,
userId :: Maybe Word64,
tags :: [Text],
attributes :: HashMap Text Text,
createdAt :: UTCTime
}

main :: IO ()
main = do
connection <-
newConnection
ConnectionOptions
{ url = "http://localhost:8123",
database = Nothing,
user = Nothing,
password = Nothing,
httpManager = Nothing
}

let rowEncoder =
contramap eventId Value.uint64
<> contramap eventName Value.string
<> contramap userId (Value.nullable Value.uint64)
<> contramap tags (Value.array Value.string)
<> contramap attributes (Value.map Value.string Value.string)
<> contramap createdAt Value.dateTime

ins =
Insert.insert
"events"
["event_id", "event_name", "user_id", "tags", "attributes", "created_at"]
rowEncoder
mempty

rows =
[ EventRow
{ eventId = 1,
eventName = "signup",
userId = Just 42,
tags = ["marketing", "trial"],
attributes = HashMap.fromList [("plan", "pro"), ("campaign", "spring-launch")],
createdAt = read "2026-03-30 01:23:45 UTC"
},
EventRow
{ eventId = 2,
eventName = "anonymous-page-view",
userId = Nothing,
tags = ["landing-page"],
attributes = HashMap.fromList [("path", "/pricing")],
createdAt = read "2026-03-30 01:25:00 UTC"
}
]

runInsert connection ins () rows
Loading
Loading