All notable changes to ch_driver are documented here.
This project adheres to Semantic Versioning.
0.4.0 - 2026-08-26
Fixed
LowCardinality(Nullable(T))columns dropped the connection with aFunctionClauseError. Unlike every otherNullable(T), a dictionary has no leading null map: ClickHouse reserves index 0 as the NULL sentinel and stores a default-valued element in slot 0 instead. Reading a null map that wasn't there consumed dictionary bytes. The sentinel is positional, so a real''or0keeps its own non-zero slot and stays distinct from NULL.
Added
Tuple(T1, ..., Tn)decoding, previously rejected with{:unsupported_type, "Tuple(...)"}. Stored element-wise (all of element 1, then all of element 2, and so on) and decoded to positional Elixir tuples. Elements may be named (Tuple(a Int32, b String)), and element types may themselves be parameterized (Tuple(Int32, Map(String, Int32))) or contribute hoisted serialization prefixes (Tuple(Array(LowCardinality(String)), String)).
0.3.0 - 2026-08-26
Fixed
Silent data corruption when decoding nested
LowCardinalitycolumns. ClickHouse hoists every serialization prefix in a column's type tree (LowCardinality's dictionary key version,Variant's discriminator mode) to the front of the column, ahead of any enclosing wrapper's data, rather than inline before the sub-stream it describes. ReadingArray(LowCardinality(String))'s key version inline consumed the first 8 bytes of the array's own offsets. This raised no error, it just mis-split the rows:[["a","b","a"], []]decoded as[["a"], ["b","a"]]. Decoding is now two-phase, popping prefixes depth-first before reading column data.Anyone querying
Array(LowCardinality(...))was silently getting wrong rows and should upgrade.Sub-second
DateTime/NaiveDateTimequery parameters declared their type asDateTimewhile rendering fractional digits, so the server rejected them withonly 19 of 26 bytes was parsed. They now declareDateTime64(P)matching the digits actually emitted.
Added
DateTime64(P[, 'TZ'])decoding. A signed little-endian Int64 tick count at 10^-P second resolution since the epoch; being signed (unlike plainDateTime's UInt32) pre-epoch instants work. Decodes to a UTCDateTime.t()at precisionmin(P, 6). The timezone argument is parsed and discarded, as it affects display only, never storage.Variant(T1, ..., Tn)decoding. One discriminator byte per row (255 = no value), then one contiguous sub-column per alternative holding only the rows that selected it.Booldecoding, as a single 0/1 byte to an Elixir boolean.- Elixir maps can be bound as query parameters, as
Map(K, V)using ClickHouse's{'k':v}literal syntax. A map with mixed value types raises anArgumentErrorup front: it would needMap(String, Variant(...)), and ClickHouse cannot parse a Variant-valued Map from parameter text at all. To pass a Variant you must nest the casts through a member type, e.g.CAST(CAST(?, 'Int32'), 'Variant(Bool, Int32, String)').
0.2.0 - 2026-08-01
Fixed
Datecolumns failed to decode:NativeBlockwas missing acolumn_codec("Date")clause.
Added
- A
:settingsoption for passing real ClickHouseSETTINGSwith a query.
0.1.1 - 2026-08-01
Changed
- Rewrote the published documentation to be usage-focused.
- Removed an unused import warning.
0.1.0 - 2026-07-31
Initial release. A DBConnection driver speaking ClickHouse's native TCP
protocol, including its LZ4/CityHash compression NIF and compressed-block
wire envelope.