diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3f3ab..cff899a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- add column names to `%Ch.Result{}` https://github.com/plausible/ch/pull/243 + ## 0.3.0 (2025-02-03) - gracefully handle `connection: closed` response from server https://github.com/plausible/ch/pull/211 diff --git a/lib/ch/query.ex b/lib/ch/query.ex index bb71fd6..f5a9805 100644 --- a/lib/ch/query.ex +++ b/lib/ch/query.ex @@ -185,8 +185,15 @@ defimpl DBConnection.Query, for: Ch.Query do %Result{num_rows: length(rows), rows: rows, command: command, headers: headers} "RowBinaryWithNamesAndTypes" -> - rows = data |> IO.iodata_to_binary() |> RowBinary.decode_rows() - %Result{num_rows: length(rows), rows: rows, command: command, headers: headers} + [names | rows] = data |> IO.iodata_to_binary() |> RowBinary.decode_names_and_rows() + + %Result{ + num_rows: length(rows), + columns: names, + rows: rows, + command: command, + headers: headers + } _other -> %Result{rows: data, data: data, command: command, headers: headers} diff --git a/lib/ch/result.ex b/lib/ch/result.ex index db75fc6..9975997 100644 --- a/lib/ch/result.ex +++ b/lib/ch/result.ex @@ -2,18 +2,20 @@ defmodule Ch.Result do @moduledoc """ Result struct returned from any successful query. Its fields are: - * `command` - An atom of the query command, for example: `:select`, `:insert`; + * `command` - An atom of the query command, for example: `:select`, `:insert` + * `columns` - A list of column names * `rows` - A list of lists, each inner list corresponding to a row, each element in the inner list corresponds to a column - * `num_rows` - The number of fetched or affected rows; + * `num_rows` - The number of fetched or affected rows * `headers` - The HTTP response headers * `data` - The raw iodata from the response """ - defstruct [:command, :num_rows, :rows, :headers, :data] + defstruct [:command, :num_rows, :columns, :rows, :headers, :data] @type t :: %__MODULE__{ - command: Ch.Query.command(), + command: Ch.Query.command() | nil, num_rows: non_neg_integer | nil, + columns: [String.t()] | nil, rows: [[term]] | iodata | nil, headers: Mint.Types.headers(), data: iodata diff --git a/lib/ch/row_binary.ex b/lib/ch/row_binary.ex index 584f603..b05f9aa 100644 --- a/lib/ch/row_binary.ex +++ b/lib/ch/row_binary.ex @@ -465,6 +465,19 @@ defmodule Ch.RowBinary do def decode_rows(<>), do: skip_names(rest, cols, cols) def decode_rows(<<>>), do: [] + @doc """ + Same as `decode_rows/1` but the first element is a list of column names. + + Example: + + iex> decode_names_and_rows(<<1, 3, "1+1"::bytes, 5, "UInt8"::bytes, 2>>) + [["1+1"], [2]] + + """ + def decode_names_and_rows(<>) do + decode_names(rest, cols, cols, _acc = []) + end + @doc """ Decodes [`RowBinary`](https://clickhouse.com/docs/en/sql-reference/formats#rowbinary) into rows. @@ -558,8 +571,6 @@ defmodule Ch.RowBinary do raise ArgumentError, "unsupported type for decoding: #{inspect(type)}" end - defp skip_names(<>, 0, count), do: decode_types(rest, count, _acc = []) - varints = [ {_pattern = quote(do: <<0::1, v1::7>>), _value = quote(do: v1)}, {quote(do: <<1::1, v1::7, 0::1, v2::7>>), quote(do: (v2 <<< 7) + v1)}, @@ -588,12 +599,29 @@ defmodule Ch.RowBinary do end} ] + defp skip_names(<>, 0, count), do: decode_types(rest, count, _acc = []) + for {pattern, value} <- varints do defp skip_names(<>, left, count) do skip_names(rest, left - 1, count) end end + defp decode_names(<>, 0, count, names) do + [:lists.reverse(names) | decode_types(rest, count, _acc = [])] + end + + for {pattern, value} <- varints do + defp decode_names( + <>, + left, + count, + acc + ) do + decode_names(rest, left - 1, count, [name | acc]) + end + end + defp decode_types(<<>>, 0, _types), do: [] defp decode_types(<>, 0, types) do diff --git a/test/ch/query_test.exs b/test/ch/query_test.exs index 72c9827..4aa592d 100644 --- a/test/ch/query_test.exs +++ b/test/ch/query_test.exs @@ -237,10 +237,18 @@ defmodule Ch.QueryTest do assert {:ok, res} = Ch.query(conn, "SELECT 123 AS a, 456 AS b") assert %Ch.Result{} = res assert res.command == :select - # assert res.columns == ["a", "b"] + assert res.columns == ["a", "b"] assert res.num_rows == 1 end + test "empty result struct", %{conn: conn} do + assert %Ch.Result{} = res = Ch.query!(conn, "select number, 'a' as b from numbers(0)") + assert res.command == :select + assert res.columns == ["number", "b"] + assert res.rows == [] + assert res.num_rows == 0 + end + test "error struct", %{conn: conn} do assert {:error, %Ch.Error{}} = Ch.query(conn, "SELECT 123 + 'a'") end