ChDriver.DBConnection (ch_driver v0.1.0)

Copy Markdown

DBConnection behaviour implementation wrapping ChDriver.Connection's synchronous request/response socket so it can be used as a normal pooled connection.

ClickHouse's native TCP protocol is a plain, unpipelined request/response protocol -- one query in flight per socket at a time -- so this module is a thin adapter: handle_execute/4 receives query (a %ChDriver.Query{} already lexed once by DBConnection.Query.parse/2) and encoded_params (this call's values, already run through DBConnection.Query.encode/3), splices them together into the final wire statement + {name, raw_text, escape_rounds} params via ChDriver.Query's to_wire/2, calls ChDriver.Connection.query/2,3, and translates its {:ok, %{columns:, rows:}} / {:error, term} shape into DBConnection's {:ok, query, result, state} / {:error | :disconnect, exception, state} contract -- result here is the raw {columns, rows} map; DBConnection.Query.decode/3 (in ch_driver/lib/ch_driver/query.ex) is what turns it into a %ChDriver.Result{}, so it can honor opts[:decode_mapper].

Socket-level failures (:gen_tcp errors such as :closed/:timeout) are treated as :disconnect so the pool tears down and reconnects the underlying socket; ClickHouse-level query errors (a decoded %ChDriver.Error{} from an Exception packet) are returned as ordinary :error results, since the connection itself is still perfectly usable for the next query.

There is no transaction support in the ClickHouse native protocol as used here, so handle_begin/2, handle_commit/2, and handle_rollback/2 are unimplemented stubs that error out rather than silently pretending to do something; handle_status/2 always reports :idle since there is no transaction state to track.

Cursors (handle_declare/4, handle_fetch/4, handle_deallocate/4)

Unlike transactions, real incremental streaming is achievable here: ClickHouse's native protocol already delivers a query's result as a header block followed by N Data blocks followed by EndOfStream, and ChDriver.Connection.start_stream/3 / stream_fetch/2 / cancel_stream/2 expose that per-block loop instead of always running it to completion and accumulating everything into memory the way handle_execute/4 does. The "cursor" handle_fetch/4 receives back from DBConnection on every call is a fixed value chosen once by handle_declare/4 (a bare reference/0, never inspected) -- DBConnection does not thread an updated cursor between fetches (see its @callback handle_fetch/4 docs: it returns new_state, not a new cursor), so the actual mutable stream position (the unconsumed buffer tail, current known columns, whether :end_of_stream has been seen) has to live in the connection state itself, under state.stream -- exactly the socket/buffer persistence this needs, since state is threaded through every callback the normal way.

handle_declare/4 is only reachable with a connection already checked out to the calling process for the whole declare/fetch*/deallocate sequence (DBConnection.stream/4's own resource/5 helper requires an already-checked-out %DBConnection{} -- see ChDriver.stream/2,3,4's moduledoc), so there's no risk of a pooled connection handing the cursor's socket to a different physical connection mid-stream.