Skip to content

Commit e0a0ebc

Browse files
committed
Merge remote-tracking branch 'origin/master' into ruslandoga-conductor/hardcore-types-ex-tests
# Conflicts: # lib/ch/types.ex # test/ch/types_test.exs
2 parents 3e21dc7 + 5c9244a commit e0a0ebc

9 files changed

Lines changed: 993 additions & 46 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- Add explicit request and response compression support through HTTP headers. `zstd` and `gzip` response bodies are decompressed automatically for decoded `RowBinaryWithNamesAndTypes` and error responses; raw successful responses are kept as received in `Ch.Result.data`.
1919
- Fix `Time64` RowBinary encoding for precisions below microseconds.
2020
- Fix RowBinary integer encoders to reject out-of-range `Int16`/`UInt16` and wider values instead of silently wrapping, with added property coverage through 256-bit integer types.
21+
- Fix `Ch.Types.encode/1` to reject empty `Enum8` and `Enum16` mappings instead of producing invalid type strings.
2122

2223
## 0.8.3 (2026-05-12)
2324

README.md

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,49 +32,45 @@ Start a pool:
3232
{:ok, pool} = Ch.start_link(url: "http://localhost:8123")
3333
```
3434

35-
Run a query with named ClickHouse parameters:
35+
Run a query:
3636

3737
```elixir
38-
%Ch.Result{names: ["number"], rows: [[0], [1] | _rest]} =
38+
%Ch.Result{names: ["number"], rows: [[0], [1], [2]]} =
3939
Ch.query!(
4040
pool,
4141
"select number from numbers({limit:UInt32})",
42-
%{"limit" => 100},
42+
%{"limit" => 3},
4343
headers: [{"accept-encoding", "zstd"}]
4444
)
4545
```
4646

47-
Insert RowBinary data by encoding it explicitly:
47+
Create a table and insert RowBinaryWithNamesAndTypes data:
4848

4949
```elixir
50-
rows = [[1, "one"], [2, "two"]]
51-
types = ["UInt8", "String"]
52-
rowbinary = Ch.RowBinary.encode_rows(rows, types)
53-
54-
Ch.query!(pool, [
55-
"INSERT INTO events FORMAT RowBinary\n",
56-
rowbinary
57-
])
58-
```
50+
session_id = "ch-demo-session"
5951

60-
Compressed inserts use the same shape, with the whole request body compressed:
52+
Ch.query!(
53+
pool,
54+
"create temporary table demo(id UInt64, text String) engine Memory",
55+
%{},
56+
settings: %{"session_id" => session_id}
57+
)
6158

62-
```elixir
63-
names = ["id", "name"]
64-
types = ["UInt8", "String"]
59+
names = ["id", "text"]
60+
types = ["UInt64", "String"]
6561
rows = [[1, "one"], [2, "two"]]
6662

67-
payload =
68-
:zstd.compress([
69-
"INSERT INTO events FORMAT RowBinaryWithNamesAndTypes\n",
70-
Ch.RowBinary.encode_names_and_types(names, types),
71-
Ch.RowBinary.encode_rows(rows, types)
72-
])
63+
insert = [
64+
"insert into demo format RowBinaryWithNamesAndTypes\n",
65+
Ch.RowBinary.encode_names_and_types(names, types)
66+
| Ch.RowBinary.encode_rows(rows, types)
67+
]
7368

7469
Ch.query!(
7570
pool,
76-
payload,
71+
:zstd.compress(insert),
7772
%{},
73+
settings: %{"session_id" => session_id},
7874
headers: [{"content-encoding", "zstd"}]
7975
)
8076
```

lib/ch/types.ex

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -632,10 +632,18 @@ defmodule Ch.Types do
632632
["DateTime64(", Integer.to_string(precision), ", '", encode_string(timezone), "')"]
633633
end
634634

635+
def encode({:enum8, []}) do
636+
raise ArgumentError, "Enum8 requires at least one mapping"
637+
end
638+
635639
def encode({:enum8, mapping}) do
636640
["Enum8('", encode_mapping(mapping), ?)]
637641
end
638642

643+
def encode({:enum16, []}) do
644+
raise ArgumentError, "Enum16 requires at least one mapping"
645+
end
646+
639647
def encode({:enum16, mapping}) do
640648
["Enum16('", encode_mapping(mapping), ?)]
641649
end
@@ -665,18 +673,14 @@ defmodule Ch.Types do
665673
defp encode_mapping([] = empty), do: empty
666674

667675
defp encode_string(string) do
668-
for <<char::utf8 <- string>> do
669-
escape_string_char(char)
670-
end
676+
string
677+
|> String.replace("\\", "\\\\")
678+
|> String.replace("'", "\\'")
679+
|> String.replace(<<0>>, "\\0")
680+
|> String.replace("\b", "\\b")
681+
|> String.replace("\f", "\\f")
682+
|> String.replace("\n", "\\n")
683+
|> String.replace("\r", "\\r")
684+
|> String.replace("\t", "\\t")
671685
end
672-
673-
defp escape_string_char(?\0), do: "\\0"
674-
defp escape_string_char(?\b), do: "\\b"
675-
defp escape_string_char(?\f), do: "\\f"
676-
defp escape_string_char(?\n), do: "\\n"
677-
defp escape_string_char(?\r), do: "\\r"
678-
defp escape_string_char(?\t), do: "\\t"
679-
defp escape_string_char(?'), do: "\\'"
680-
defp escape_string_char(?\\), do: "\\\\"
681-
defp escape_string_char(char), do: <<char::utf8>>
682686
end

test/ch/faults_test.exs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ defmodule Ch.FaultsTest do
44
@socket_opts [:binary, {:active, true}, {:packet, :raw}]
55

66
setup do
7-
{:ok, clickhouse} = :gen_tcp.connect({127, 0, 0, 1}, 8123, @socket_opts)
87
{:ok, listen} = :gen_tcp.listen(0, @socket_opts)
98
{:ok, port} = :inet.port(listen)
10-
{:ok, clickhouse: clickhouse, listen: listen, port: port}
9+
{:ok, listen: listen, port: port}
1110
end
1211

1312
test "returns transport errors when ClickHouse is unreachable", %{listen: listen, port: port} do
@@ -21,17 +20,20 @@ defmodule Ch.FaultsTest do
2120
end
2221

2322
test "removes a timed out connection and reconnects on the next query", ctx do
24-
%{port: port, listen: listen, clickhouse: clickhouse} = ctx
23+
%{port: port, listen: listen} = ctx
2524
{:ok, pool} = Ch.start_link(url: "http://localhost:#{port}")
2625

26+
clickhouse = connect_clickhouse!()
27+
2728
select =
2829
Task.async(fn ->
29-
Ch.query(pool, "select 1 + 1", %{}, timeout: 100)
30+
Ch.query(pool, "select 1 + 1", %{}, timeout: 500)
3031
end)
3132

3233
{:ok, mint} = :gen_tcp.accept(listen)
3334
:ok = :gen_tcp.send(clickhouse, read_packets(mint))
3435
:ok = :gen_tcp.send(mint, first_byte(read_packets(clickhouse)))
36+
:ok = :gen_tcp.close(clickhouse)
3537

3638
assert {:error, %Mint.TransportError{reason: :timeout}} = Task.await(select)
3739

@@ -40,26 +42,30 @@ defmodule Ch.FaultsTest do
4042
Ch.query(pool, "select 1 + 1", %{}, timeout: 1_000)
4143
end)
4244

45+
clickhouse = connect_clickhouse!()
4346
{:ok, mint} = :gen_tcp.accept(listen)
4447
:ok = :gen_tcp.send(clickhouse, read_packets(mint))
4548
:ok = :gen_tcp.send(mint, read_packets(clickhouse))
49+
:ok = :gen_tcp.close(clickhouse)
4650

4751
assert {:ok, %{rows: [[2]]}} = Task.await(select)
4852
end
4953

5054
test "removes a closed connection and reconnects on the next query", ctx do
51-
%{port: port, listen: listen, clickhouse: clickhouse} = ctx
55+
%{port: port, listen: listen} = ctx
5256
{:ok, pool} = Ch.start_link(url: "http://localhost:#{port}")
5357

5458
select =
5559
Task.async(fn ->
5660
Ch.query(pool, "select 1 + 1", %{}, timeout: 1_000)
5761
end)
5862

63+
clickhouse = connect_clickhouse!()
5964
{:ok, mint} = :gen_tcp.accept(listen)
6065
:ok = :gen_tcp.send(clickhouse, read_packets(mint))
6166
:ok = :gen_tcp.send(mint, first_byte(read_packets(clickhouse)))
6267
:ok = :gen_tcp.close(mint)
68+
:ok = :gen_tcp.close(clickhouse)
6369

6470
assert {:error, %Mint.TransportError{reason: :closed}} = Task.await(select)
6571

@@ -68,13 +74,20 @@ defmodule Ch.FaultsTest do
6874
Ch.query(pool, "select 1 + 1", %{}, timeout: 1_000)
6975
end)
7076

77+
clickhouse = connect_clickhouse!()
7178
{:ok, mint} = :gen_tcp.accept(listen)
7279
:ok = :gen_tcp.send(clickhouse, read_packets(mint))
7380
:ok = :gen_tcp.send(mint, read_packets(clickhouse))
81+
:ok = :gen_tcp.close(clickhouse)
7482

7583
assert {:ok, %{rows: [[2]]}} = Task.await(select)
7684
end
7785

86+
defp connect_clickhouse! do
87+
{:ok, clickhouse} = :gen_tcp.connect({127, 0, 0, 1}, 8123, @socket_opts)
88+
clickhouse
89+
end
90+
7891
defp read_packets(socket) do
7992
receive do
8093
{:tcp, ^socket, packet} -> read_packets(socket, packet)
@@ -87,7 +100,7 @@ defmodule Ch.FaultsTest do
87100
{:tcp, ^socket, packet} -> read_packets(socket, [acc | packet])
88101
{:tcp_closed, ^socket} -> acc
89102
after
90-
10 -> acc
103+
50 -> acc
91104
end
92105
end
93106

test/ch/http_test.exs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ defmodule Ch.HTTPTest do
2121
{:deadline, round_tripped_timestamp} =
2222
deadline |> Ch.HTTP.to_timeout() |> Ch.HTTP.to_deadline()
2323

24-
assert round_tripped_timestamp <= original_timestamp
25-
assert round_tripped_timestamp >= original_timestamp - 50
24+
assert_in_delta round_tripped_timestamp, original_timestamp, 50
2625
end
2726
end
2827
end

0 commit comments

Comments
 (0)