|
1 | 1 | defmodule Ch.Query do |
2 | 2 | @moduledoc "Query struct wrapping the SQL statement." |
3 | | - defstruct [:statement, :command, :encode, :decode] |
| 3 | + defstruct [:statement, :command, :encode, :decode, :multipart] |
4 | 4 |
|
5 | | - @type t :: %__MODULE__{statement: iodata, command: command, encode: boolean, decode: boolean} |
| 5 | + @type t :: %__MODULE__{ |
| 6 | + statement: iodata, |
| 7 | + command: command, |
| 8 | + encode: boolean, |
| 9 | + decode: boolean, |
| 10 | + multipart: boolean |
| 11 | + } |
6 | 12 |
|
7 | 13 | @doc false |
8 | 14 | @spec build(iodata, [Ch.query_option()]) :: t |
9 | 15 | def build(statement, opts \\ []) do |
10 | 16 | command = Keyword.get(opts, :command) || extract_command(statement) |
11 | 17 | encode = Keyword.get(opts, :encode, true) |
12 | 18 | decode = Keyword.get(opts, :decode, true) |
13 | | - %__MODULE__{statement: statement, command: command, encode: encode, decode: decode} |
| 19 | + multipart = Keyword.get(opts, :multipart, false) |
| 20 | + |
| 21 | + %__MODULE__{ |
| 22 | + statement: statement, |
| 23 | + command: command, |
| 24 | + encode: encode, |
| 25 | + decode: decode, |
| 26 | + multipart: multipart |
| 27 | + } |
14 | 28 | end |
15 | 29 |
|
16 | 30 | statements = [ |
@@ -72,6 +86,7 @@ defmodule Ch.Query do |
72 | 86 | end |
73 | 87 |
|
74 | 88 | defimpl DBConnection.Query, for: Ch.Query do |
| 89 | + @dialyzer :no_improper_lists |
75 | 90 | alias Ch.{Query, Result, RowBinary} |
76 | 91 |
|
77 | 92 | @spec parse(Query.t(), [Ch.query_option()]) :: Query.t() |
@@ -128,13 +143,82 @@ defimpl DBConnection.Query, for: Ch.Query do |
128 | 143 | end |
129 | 144 | end |
130 | 145 |
|
| 146 | + def encode(%Query{multipart: true, statement: statement}, params, opts) do |
| 147 | + types = Keyword.get(opts, :types) |
| 148 | + default_format = if types, do: "RowBinary", else: "RowBinaryWithNamesAndTypes" |
| 149 | + format = Keyword.get(opts, :format) || default_format |
| 150 | + |
| 151 | + boundary = "ChFormBoundary" <> Base.url_encode64(:crypto.strong_rand_bytes(24)) |
| 152 | + content_type = "multipart/form-data; boundary=\"#{boundary}\"" |
| 153 | + enc_boundary = "--#{boundary}\r\n" |
| 154 | + multipart = multipart_params(params, enc_boundary) |
| 155 | + multipart = add_multipart_part(multipart, "query", statement, enc_boundary) |
| 156 | + multipart = [multipart | "--#{boundary}--\r\n"] |
| 157 | + |
| 158 | + {_no_query_params = [], |
| 159 | + [{"x-clickhouse-format", format}, {"content-type", content_type} | headers(opts)], multipart} |
| 160 | + end |
| 161 | + |
131 | 162 | def encode(%Query{statement: statement}, params, opts) do |
132 | 163 | types = Keyword.get(opts, :types) |
133 | 164 | default_format = if types, do: "RowBinary", else: "RowBinaryWithNamesAndTypes" |
134 | 165 | format = Keyword.get(opts, :format) || default_format |
135 | 166 | {query_params(params), [{"x-clickhouse-format", format} | headers(opts)], statement} |
136 | 167 | end |
137 | 168 |
|
| 169 | + defp multipart_params(params, boundary) when is_map(params) do |
| 170 | + multipart_named_params(Map.to_list(params), boundary, []) |
| 171 | + end |
| 172 | + |
| 173 | + defp multipart_params(params, boundary) when is_list(params) do |
| 174 | + multipart_positional_params(params, 0, boundary, []) |
| 175 | + end |
| 176 | + |
| 177 | + defp multipart_named_params([{name, value} | params], boundary, acc) do |
| 178 | + acc = |
| 179 | + add_multipart_part( |
| 180 | + acc, |
| 181 | + "param_" <> URI.encode_www_form(name), |
| 182 | + encode_param(value), |
| 183 | + boundary |
| 184 | + ) |
| 185 | + |
| 186 | + multipart_named_params(params, boundary, acc) |
| 187 | + end |
| 188 | + |
| 189 | + defp multipart_named_params([], _boundary, acc), do: acc |
| 190 | + |
| 191 | + defp multipart_positional_params([value | params], idx, boundary, acc) do |
| 192 | + acc = |
| 193 | + add_multipart_part( |
| 194 | + acc, |
| 195 | + "param_$" <> Integer.to_string(idx), |
| 196 | + encode_param(value), |
| 197 | + boundary |
| 198 | + ) |
| 199 | + |
| 200 | + multipart_positional_params(params, idx + 1, boundary, acc) |
| 201 | + end |
| 202 | + |
| 203 | + defp multipart_positional_params([], _idx, _boundary, acc), do: acc |
| 204 | + |
| 205 | + @compile inline: [add_multipart_part: 4] |
| 206 | + defp add_multipart_part(multipart, name, value, boundary) do |
| 207 | + part = [ |
| 208 | + boundary, |
| 209 | + "content-disposition: form-data; name=\"", |
| 210 | + name, |
| 211 | + "\"\r\n\r\n", |
| 212 | + value, |
| 213 | + "\r\n" |
| 214 | + ] |
| 215 | + |
| 216 | + case multipart do |
| 217 | + [] -> part |
| 218 | + _ -> [multipart | part] |
| 219 | + end |
| 220 | + end |
| 221 | + |
138 | 222 | defp format_row_binary?(statement) when is_binary(statement) do |
139 | 223 | statement |> String.trim_trailing() |> String.ends_with?("RowBinary") |
140 | 224 | end |
|
0 commit comments