Skip to content

Commit 1275855

Browse files
authored
Merge pull request #240 from plausible/raw-api
new raw api
2 parents 0a8ca00 + 790de76 commit 1275855

16 files changed

+414
-381
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- move rows for INSERT from `params` to `statement` for more control (compression, format, query params, etc.)
6+
37
## 0.3.0 (2025-02-03)
48

59
- gracefully handle `connection: closed` response from server https://github.com/plausible/ch/pull/211

README.md

Lines changed: 55 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,54 @@ Minimal HTTP [ClickHouse](https://clickhouse.com) client for Elixir.
77

88
Used in [Ecto ClickHouse adapter.](https://github.com/plausible/ecto_ch)
99

10-
### Key features
11-
12-
- RowBinary
13-
- Native query parameters
14-
- Per query settings
15-
- Minimal API
16-
17-
Your ideas are welcome [here.](https://github.com/plausible/ch/issues/82)
18-
1910
## Installation
2011

2112
```elixir
2213
defp deps do
2314
[
24-
{:ch, "~> 0.3.0"}
15+
{:ch, "~> 0.4.0"}
2516
]
2617
end
2718
```
2819

2920
## Usage
3021

22+
#### Start ClickHouse
23+
24+
```sh
25+
# don't forget to stop the container once done
26+
docker run --rm -p 8123:8123 -e CLICKHOUSE_PASSWORD=secret --ulimit nofile=262144:262144 clickhouse/clickhouse-server:latest-alpine
27+
```
28+
3129
#### Start [DBConnection](https://github.com/elixir-ecto/db_connection) pool
3230

3331
```elixir
3432
defaults = [
3533
scheme: "http",
3634
hostname: "localhost",
3735
port: 8123,
38-
database: "default",
36+
database: "default",
3937
settings: [],
4038
pool_size: 1,
4139
timeout: :timer.seconds(15)
4240
]
4341

44-
# note that starting in ClickHouse 25.1.3.23 `default` user doesn't have
45-
# network access by default in the official Docker images
46-
# see https://github.com/ClickHouse/ClickHouse/pull/75259
47-
{:ok, pid} = Ch.start_link(defaults)
42+
custom = [
43+
# note that starting in ClickHouse 25.1.3.23 `default` user doesn't have
44+
# network access by default in the official Docker images
45+
# see https://github.com/ClickHouse/ClickHouse/pull/75259
46+
username: "default",
47+
# this password was provided via `CLICKHOUSE_PASSWORD` to the container above
48+
password: "secret",
49+
]
50+
51+
config = Keyword.merge(defaults, custom)
52+
{:ok, pid} = Ch.start_link(config)
4853
```
4954

5055
#### Select rows
5156

5257
```elixir
53-
{:ok, pid} = Ch.start_link()
54-
5558
{:ok, %Ch.Result{rows: [[0], [1], [2]]}} =
5659
Ch.query(pid, "SELECT * FROM system.numbers LIMIT 3")
5760

@@ -70,70 +73,76 @@ Note on datetime encoding in query parameters:
7073
#### Insert rows
7174

7275
```elixir
73-
{:ok, pid} = Ch.start_link()
74-
7576
Ch.query!(pid, "CREATE TABLE IF NOT EXISTS ch_demo(id UInt64) ENGINE Null")
7677

7778
%Ch.Result{num_rows: 2} =
7879
Ch.query!(pid, "INSERT INTO ch_demo(id) VALUES (0), (1)")
7980

80-
%Ch.Result{num_rows: 2} =
81-
Ch.query!(pid, "INSERT INTO ch_demo(id) VALUES ({$0:UInt8}), ({$1:UInt32})", [0, 1])
82-
8381
%Ch.Result{num_rows: 2} =
8482
Ch.query!(pid, "INSERT INTO ch_demo(id) VALUES ({a:UInt16}), ({b:UInt64})", %{"a" => 0, "b" => 1})
8583

8684
%Ch.Result{num_rows: 2} =
8785
Ch.query!(pid, "INSERT INTO ch_demo(id) SELECT number FROM system.numbers LIMIT {limit:UInt8}", %{"limit" => 2})
8886
```
8987

90-
#### Insert rows as [RowBinary](https://clickhouse.com/docs/en/interfaces/formats#rowbinary) (efficient)
88+
#### Insert rows as [RowBinary](https://clickhouse.com/docs/en/interfaces/formats/RowBinary) (efficient)
9189

9290
```elixir
93-
{:ok, pid} = Ch.start_link()
91+
Ch.query!(pid, "CREATE TABLE IF NOT EXISTS ch_demo(id UInt64, name String) ENGINE Null")
9492

95-
Ch.query!(pid, "CREATE TABLE IF NOT EXISTS ch_demo(id UInt64) ENGINE Null")
93+
rows = [
94+
[0, "zero"],
95+
[1, "one"],
96+
[2, "two"]
97+
]
9698

97-
types = ["UInt64"]
98-
# or
99-
types = [Ch.Types.u64()]
100-
# or
101-
types = [:u64]
99+
types = ["UInt64", "String"]
100+
# or types = [:u64, :string]
101+
# or types = [Ch.Types.u64(), Ch.Types.string()]
102102

103-
%Ch.Result{num_rows: 2} =
104-
Ch.query!(pid, "INSERT INTO ch_demo(id) FORMAT RowBinary", [[0], [1]], types: types)
105-
```
103+
sql = [
104+
# note the \n -- ClickHouse uses it to separate SQL from the data
105+
"INSERT INTO ch_demo(id, name) FORMAT RowBinary\n" | Ch.RowBinary.encode_rows(rows, types)
106+
]
106107

107-
Note that RowBinary format encoding requires `:types` option to be provided.
108+
%Ch.Result{num_rows: 2} = Ch.query!(pid, sql)
109+
```
108110

109-
Similarly, you can use [`RowBinaryWithNamesAndTypes`](https://clickhouse.com/docs/en/interfaces/formats#rowbinarywithnamesandtypes) which would additionally do something like a type check.
111+
Similarly, you can use [`RowBinaryWithNamesAndTypes`](https://clickhouse.com/docs/en/interfaces/formats/RowBinaryWithNamesAndTypes) which would additionally do something like a type check.
110112

111113
```elixir
112-
sql = "INSERT INTO ch_demo FORMAT RowBinaryWithNamesAndTypes"
113-
opts = [names: ["id"], types: ["UInt64"]]
114-
rows = [[0], [1]]
114+
names = ["id", "name"]
115+
types = ["UInt64", "String"]
116+
117+
rows = [
118+
[0, "zero"],
119+
[1, "one"],
120+
[2, "two"]
121+
]
122+
123+
sql = [
124+
"INSERT INTO ch_demo FORMAT RowBinaryWithNamesAndTypes\n",
125+
Ch.RowBinary.encode_names_and_types(names, types),
126+
| Ch.RowBinary.encode_rows(rows, types)
127+
]
115128

116-
%Ch.Result{num_rows: 2} = Ch.query!(pid, sql, rows, opts)
129+
%Ch.Result{num_rows: 2} = Ch.query!(pid, sql)
117130
```
118131

119132
#### Insert rows in custom [format](https://clickhouse.com/docs/en/interfaces/formats)
120133

121134
```elixir
122-
{:ok, pid} = Ch.start_link()
123-
124135
Ch.query!(pid, "CREATE TABLE IF NOT EXISTS ch_demo(id UInt64) ENGINE Null")
125136

126137
csv = [0, 1] |> Enum.map(&to_string/1) |> Enum.intersperse(?\n)
127138

128139
%Ch.Result{num_rows: 2} =
129-
Ch.query!(pid, "INSERT INTO ch_demo(id) FORMAT CSV", csv, encode: false)
140+
Ch.query!(pid, ["INSERT INTO ch_demo(id) FORMAT CSV\n" | csv])
130141
```
131142

132143
#### Insert rows as chunked RowBinary stream
133144

134145
```elixir
135-
{:ok, pid} = Ch.start_link()
136-
137146
Ch.query!(pid, "CREATE TABLE IF NOT EXISTS ch_demo(id UInt64) ENGINE Null")
138147

139148
stream = Stream.repeatedly(fn -> [:rand.uniform(100)] end)
@@ -150,8 +159,6 @@ This query makes a [`transfer-encoding: chunked`](https://en.wikipedia.org/wiki/
150159
#### Query with custom [settings](https://clickhouse.com/docs/en/operations/settings/settings)
151160

152161
```elixir
153-
{:ok, pid} = Ch.start_link()
154-
155162
settings = [async_insert: 1]
156163

157164
%Ch.Result{rows: [["async_insert", "Bool", "0"]]} =
@@ -208,6 +215,8 @@ Ch.query!(pid, sql, inserted_rows, types: ["Nullable(UInt8)", "Nullable(UInt8)",
208215
Ch.query!(pid, "SELECT b FROM ch_nulls ORDER BY b")
209216
```
210217

218+
Or [`RowBinaryWithDefaults`](https://clickhouse.com/docs/en/interfaces/formats/RowBinaryWithDefaults). TODO.
219+
211220
#### UTF-8 in RowBinary
212221

213222
When decoding [`String`](https://clickhouse.com/docs/en/sql-reference/data-types/string) columns non UTF-8 characters are replaced with `` (U+FFFD). This behaviour is similar to [`toValidUTF8`](https://clickhouse.com/docs/en/sql-reference/functions/string-functions#tovalidutf8) and [JSON format.](https://clickhouse.com/docs/en/interfaces/formats#json)

guides/buffer.md

Whitespace-only changes.

guides/compression.md

Whitespace-only changes.

guides/format.md

Whitespace-only changes.

lib/ch.ex

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ defmodule Ch do
1414
| {:scheme, String.t()}
1515
| {:hostname, String.t()}
1616
| {:port, :inet.port_number()}
17-
| {:transport_opts, :gen_tcp.connect_option()}
17+
| {:transport_opts, [:gen_tcp.connect_option() | :ssl.client_option()]}
1818
| DBConnection.start_option()
1919

2020
@doc """
@@ -56,11 +56,12 @@ defmodule Ch do
5656
| {:headers, [{String.t(), String.t()}]}
5757
| {:format, String.t()}
5858
| {:types, [String.t() | atom | tuple]}
59-
# TODO remove
60-
| {:encode, boolean}
6159
| {:decode, boolean}
6260
| DBConnection.connection_option()
6361

62+
@type query_param_key :: String.t() | atom
63+
@type query_params :: %{query_param_key => term} | [term]
64+
6465
@doc """
6566
Runs a query and returns the result as `{:ok, %Ch.Result{}}` or
6667
`{:error, Exception.t()}` if there was a database error.
@@ -79,9 +80,8 @@ defmodule Ch do
7980
* [`DBConnection.connection_option()`](https://hexdocs.pm/db_connection/DBConnection.html#t:connection_option/0)
8081
8182
"""
82-
@spec query(DBConnection.conn(), iodata, params, [query_option]) ::
83+
@spec query(DBConnection.conn(), iodata, Ch.query_params(), [query_option]) ::
8384
{:ok, Result.t()} | {:error, Exception.t()}
84-
when params: map | [term] | [row :: [term]] | iodata | Enumerable.t()
8585
def query(conn, statement, params \\ [], opts \\ []) do
8686
query = Query.build(statement, opts)
8787

@@ -94,15 +94,14 @@ defmodule Ch do
9494
Runs a query and returns the result or raises `Ch.Error` if
9595
there was an error. See `query/4`.
9696
"""
97-
@spec query!(DBConnection.conn(), iodata, params, [query_option]) :: Result.t()
98-
when params: map | [term] | [row :: [term]] | iodata | Enumerable.t()
97+
@spec query!(DBConnection.conn(), iodata, Ch.query_params(), [query_option]) :: Result.t()
9998
def query!(conn, statement, params \\ [], opts \\ []) do
10099
query = Query.build(statement, opts)
101100
DBConnection.execute!(conn, query, params, opts)
102101
end
103102

104103
@doc false
105-
@spec stream(DBConnection.t(), iodata, map | [term], [query_option]) :: Ch.Stream.t()
104+
@spec stream(DBConnection.t(), iodata, Ch.query_params(), [query_option]) :: Ch.Stream.t()
106105
def stream(conn, statement, params \\ [], opts \\ []) do
107106
query = Query.build(statement, opts)
108107
%Ch.Stream{conn: conn, query: query, params: params, opts: opts}

lib/ch/connection.ex

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -212,25 +212,6 @@ defmodule Ch.Connection do
212212
end
213213
end
214214

215-
def handle_execute(%Query{command: :insert} = query, params, opts, conn) do
216-
conn = maybe_reconnect(conn)
217-
{query_params, extra_headers, body} = params
218-
219-
path = path(conn, query_params, opts)
220-
headers = headers(conn, extra_headers, opts)
221-
222-
result =
223-
if is_function(body, 2) do
224-
request_chunked(conn, "POST", path, headers, body, opts)
225-
else
226-
request(conn, "POST", path, headers, body, opts)
227-
end
228-
229-
with {:ok, conn, responses} <- result do
230-
{:ok, query, responses, conn}
231-
end
232-
end
233-
234215
def handle_execute(query, params, opts, conn) do
235216
conn = maybe_reconnect(conn)
236217
{query_params, extra_headers, body} = params
@@ -261,33 +242,6 @@ defmodule Ch.Connection do
261242
end
262243
end
263244

264-
@spec request_chunked(conn, binary, binary, Mint.Types.headers(), Enumerable.t(), Keyword.t()) ::
265-
{:ok, conn, [response]}
266-
| {:error, Error.t(), conn}
267-
| {:disconnect, Mint.Types.error(), conn}
268-
def request_chunked(conn, method, path, headers, stream, opts) do
269-
with {:ok, conn, ref} <- send_request(conn, method, path, headers, :stream),
270-
{:ok, conn} <- stream_body(conn, ref, stream),
271-
do: receive_full_response(conn, timeout(conn, opts))
272-
end
273-
274-
@spec stream_body(conn, Mint.Types.request_ref(), Enumerable.t()) ::
275-
{:ok, conn} | {:disconnect, Mint.Types.error(), conn}
276-
defp stream_body(conn, ref, stream) do
277-
result =
278-
stream
279-
|> Stream.concat([:eof])
280-
|> Enum.reduce_while({:ok, conn}, fn
281-
chunk, {:ok, conn} -> {:cont, HTTP.stream_request_body(conn, ref, chunk)}
282-
_chunk, {:error, _conn, _reason} = error -> {:halt, error}
283-
end)
284-
285-
case result do
286-
{:ok, _conn} = ok -> ok
287-
{:error, conn, reason} -> {:disconnect, reason, conn}
288-
end
289-
end
290-
291245
# stacktrace is a bit cleaner with this function inlined
292246
@compile inline: [send_request: 5]
293247
defp send_request(conn, method, path, headers, body) do

0 commit comments

Comments
 (0)