• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

plausible / ch / f336d0099eb057d381f083e12434655826548ab2-PR-391

27 Jul 2026 02:04PM UTC coverage: 98.052%. Remained the same
f336d0099eb057d381f083e12434655826548ab2-PR-391

Pull #391

github

ruslandoga
add fix
Pull Request #391: Fix error code

0 of 1 new or added line in 1 file covered. (0.0%)

755 of 770 relevant lines covered (98.05%)

15511.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.15
/lib/ch.ex
1
defmodule Ch do
2
  @moduledoc """
3
  Minimal HTTP ClickHouse client.
4

5
  `Ch` starts a lazy pool of HTTP/1 connections to ClickHouse. The pool opens
6
  connections on demand and reuses them while they remain healthy.
7

8
  By default, queries request `RowBinaryWithNamesAndTypes` and return decoded
9
  rows:
10

11
      {:ok, pool} = Ch.start_link(url: "http://localhost:8123")
12

13
      {:ok, %Ch.Result{names: ["number"], rows: [[1], [2], [3]]}} =
14
        Ch.query(pool, "SELECT number FROM system.numbers LIMIT {limit:UInt8}", %{
15
          "limit" => 3
16
        })
17

18
  For large decoded responses, ask ClickHouse for compressed response bodies:
19

20
      Ch.query!(
21
        pool,
22
        "SELECT number FROM system.numbers LIMIT 1_000_000",
23
        %{},
24
        headers: [{"accept-encoding", "zstd"}]
25
      )
26

27
  `Ch` automatically decompresses successful responses that it decodes itself
28
  (`RowBinaryWithNamesAndTypes`) and error responses. Successful responses in
29
  other formats keep the raw response body in `Ch.Result.data`, including any
30
  `content-encoding`.
31
  """
32
  @behaviour NimblePool
33

34
  @dialyzer :no_improper_lists
35

36
  @query_timeout to_timeout(second: 30)
37
  @user_agent "ch/#{Ch.MixProject.version()}"
38

39
  @start_options_schema [
40
    name: [
41
      type: {:custom, __MODULE__, :validate_name, []},
42
      doc: """
43
      The name of the Ch pool instance, used to identify and interact with it. Supported values are atoms and via tuples.
44
      """
45
    ],
46
    pool_size: [
47
      type: :pos_integer,
48
      doc:
49
        "Maximum number of concurrent connections. Pool is lazy so it starts out without any connections and they are open on demand.",
50
      default: 20
51
    ],
52
    worker_idle_timeout: [
53
      type: :timeout,
54
      doc: """
55
      Time a connection can stay idle before the pool closes it.
56
      Should be lower than ClickHouse's [`keep_alive_timeout`](https://clickhouse.com/docs/operations/server-configuration-parameters/settings#keep_alive_timeout)
57
      to avoid sending a request over a connection that would be closed by ClickHouse soon-ish.
58
      """,
59
      default: to_timeout(second: 5)
60
    ],
61
    url: [
62
      type: :string,
63
      doc: "The ClickHouse endpoint URL.",
64
      default: "http://localhost:8123"
65
    ]
66
  ]
67

68
  @doc false
69
  def validate_name(name) when is_atom(name), do: {:ok, name}
2✔
70
  def validate_name({:via, module, _term} = via) when is_atom(module), do: {:ok, via}
1✔
71

72
  def validate_name(name) do
1✔
73
    {:error,
74
     "expected :name to be an atom or a {:via, module, term} tuple, got: #{inspect(name)}"}
75
  end
76

77
  @typedoc """
78
  The query payload.
79

80
  This can be a standard SQL string or SQL appended with data (`[sql, ?\n, rowbinary]`).
81
  If providing compressed payloads, don't forget to pass the appropriate `content-encoding` header.
82
  """
83
  @type query_statement :: iodata
84

85
  @typedoc """
86
  Named query parameters.
87

88
  Keys are parameter names without the ClickHouse `param_` prefix. Values are
89
  encoded in ClickHouse's escaped parameter format and passed in the URL query
90
  string.
91

92
      Ch.query(pool, "SELECT {name:String}", %{"name" => "Ada"})
93
  """
94
  @type query_params :: %{String.t() => term}
95

96
  @typedoc """
97
  Query execution options.
98

99
  * `:timeout` - Request timeout, defaults to 30 seconds.
100
  * `:settings` - An enumerable (usually a map or a keyword list) added to the URL query string.
101
  * `:headers` - Headers passed directly to Mint.
102
  """
103
  @type query_option ::
104
          {:timeout, timeout}
105
          | {:settings, Enumerable.t()}
106
          | {:headers, Mint.Types.headers()}
107

108
  @typedoc """
109
  The parsed query response.
110

111
  If the response format is `RowBinaryWithNamesAndTypes`, `Ch` returns decoded
112
  column names and rows in `Ch.Result`. Other successful formats keep the raw
113
  response body in `Ch.Result.data`.
114
  """
115
  @type query_result :: Ch.Result.t()
116

117
  @typedoc """
118
  A query execution error.
119

120
  Returns `Ch.Error` for ClickHouse errors or Mint errors for network/HTTP failures.
121
  """
122
  @type query_error :: Ch.Error.t() | Mint.Types.error()
123

124
  @typedoc """
125
  The options supported by `start_link/1`.
126
  """
127
  @type start_option :: unquote(NimbleOptions.option_typespec(@start_options_schema))
128

129
  @doc """
130
  Starts a new Ch pool process.
131

132
  Supported options:
133
  #{NimbleOptions.docs(@start_options_schema)}
134
  """
135
  @spec start_link([start_option]) :: GenServer.on_start()
136
  def start_link(options \\ []) do
137
    options = NimbleOptions.validate!(options, @start_options_schema)
164✔
138

139
    name = Keyword.get(options, :name)
163✔
140
    pool_size = Keyword.fetch!(options, :pool_size)
163✔
141
    worker_idle_timeout = Keyword.fetch!(options, :worker_idle_timeout)
163✔
142
    url = Keyword.fetch!(options, :url)
163✔
143

144
    %URI{scheme: scheme, host: host, port: port} = URI.parse(url)
163✔
145

146
    scheme =
163✔
147
      case scheme do
148
        "http" -> :http
161✔
149
        "https" -> :https
1✔
150
        _other -> raise ArgumentError, "unexpected HTTP scheme: #{inspect(scheme)}"
1✔
151
      end
152

153
    initial_pool_state = %{
162✔
154
      template: {:template, scheme, host, port}
155
    }
156

157
    NimblePool.start_link(
162✔
158
      worker: {__MODULE__, initial_pool_state},
159
      pool_size: pool_size,
160
      worker_idle_timeout: worker_idle_timeout,
161
      lazy: true,
162
      name: name
163
    )
164
  end
165

166
  @doc """
167
  Returns a child spec to allow Ch pool to be started under a supervisor.
168

169
  ## Options
170

171
  The options are exactly the same as for `start_link/1`.
172
  """
173
  @spec child_spec([start_option]) :: Supervisor.child_spec()
174
  def child_spec(options) do
175
    %{id: __MODULE__, start: {__MODULE__, :start_link, [options]}}
143✔
176
  end
177

178
  @doc """
179
  Stops the given `pool`.
180

181
  The pool exits with the given `reason`. The pool has `timeout` milliseconds to stop
182
  before it's unilaterally killed by the runtime.
183
  """
184
  @spec stop(NimblePool.pool(), reason :: term, timeout) :: :ok
185
  def stop(pool, reason \\ :normal, timeout \\ :infinity) do
186
    NimblePool.stop(pool, reason, timeout)
14✔
187
  end
188

189
  @doc """
190
  Executes a ClickHouse query.
191

192
  `statement` is usually a SQL string. It can also be iodata, which is useful
193
  for `INSERT ... FORMAT RowBinary` requests:
194

195
      rowbinary = Ch.RowBinary.encode_rows([[1, "Ada"]], ["UInt8", "String"])
196
      Ch.query!(pool, ["INSERT INTO users FORMAT RowBinary\n", rowbinary])
197

198
  `params` are named ClickHouse query parameters used by placeholders such as
199
  `{limit:UInt8}`.
200

201
  Options:
202

203
    * `:timeout` - request timeout, defaults to 30 seconds.
204
    * `:settings` - ClickHouse settings added to the URL query string.
205
    * `:headers` - HTTP headers sent with the request.
206

207
  By default, `Ch` adds `x-clickhouse-format: RowBinaryWithNamesAndTypes`,
208
  decodes that response format, and returns `%Ch.Result{names: names, rows: rows}`.
209
  Passing a different `x-clickhouse-format` header disables automatic row
210
  decoding and keeps the response body in `%Ch.Result{data: data}`.
211

212
  If an error response is compressed with `gzip` or `zstd`, `Ch` decompresses it
213
  before returning `%Ch.Error{}`.
214
  """
215
  @spec query(NimblePool.pool(), query_statement, query_params, [query_option]) ::
216
          {:ok, query_result} | {:error, query_error}
217
  def query(pool, statement, params \\ %{}, options \\ []) do
218
    timeout = Keyword.get(options, :timeout, @query_timeout)
4,424✔
219
    settings = Keyword.get(options, :settings, [])
4,424✔
220

221
    headers =
4,424✔
222
      options
223
      |> Keyword.get(:headers, [])
224
      |> put_new_header("user-agent", @user_agent)
225
      |> put_new_header("x-clickhouse-format", "RowBinaryWithNamesAndTypes")
226

227
    deadline = Ch.HTTP.to_deadline(timeout)
4,424✔
228
    path = Ch.HTTP.path(params, settings)
4,424✔
229

230
    result =
4,422✔
231
      NimblePool.checkout!(
232
        pool,
233
        :request,
234
        fn {pid, _ref}, conn_or_template ->
235
          with {:ok, conn} <- connect(conn_or_template, pid, deadline),
4,422✔
236
               {:ok, conn, status, headers, data} <-
4,421✔
237
                 request(conn, "POST", path, headers, statement, deadline) do
238
            {{:ok, status, headers, data}, checkin(conn)}
239
          else
240
            {:error, reason} = error -> {error, {:remove, reason}}
3✔
241
          end
242
        end,
243
        timeout
244
      )
245

246
    with {:ok, status, headers, data} <- result do
4,422✔
247
      decode_query_response(status, headers, data)
4,419✔
248
    end
249
  end
250

251
  @doc """
252
  Executes a query on the given pool, raising on error.
253

254
  Returns the `query_result` directly. Raises an exception if the query fails.
255
  """
256
  @spec query!(NimblePool.pool(), query_statement, query_params, [query_option]) :: query_result
257
  def query!(pool, statement, params \\ %{}, options \\ []) do
258
    case query(pool, statement, params, options) do
4,394✔
259
      {:ok, result} -> result
4,377✔
260
      {:error, error} -> raise error
2✔
261
    end
262
  end
263

264
  @impl NimblePool
265
  def init_pool(config) do
162✔
266
    {:ok, config}
267
  end
268

269
  @impl NimblePool
270
  def init_worker(config) do
271
    {:ok, :template, config}
180✔
272
  end
273

274
  @impl NimblePool
275
  def handle_checkout(:request, _from, :template = template, config) do
276
    {:ok, config.template, template, config}
180✔
277
  end
278

279
  def handle_checkout(:request, _from, %Mint.HTTP1{} = conn, config) do
280
    {:ok, {:ok, conn}, conn, config}
4,242✔
281
  end
282

283
  @impl NimblePool
284
  def handle_checkin({:ok, conn}, _from, _prev, config) do
285
    {:ok, conn, config}
4,419✔
286
  end
287

288
  def handle_checkin({:remove, reason}, _from, _prev, config) do
289
    {:remove, reason, config}
3✔
290
  end
291

292
  @impl NimblePool
293
  def handle_ping(_conn, _config) do
2✔
294
    {:remove, :worker_idle_timeout}
295
  end
296

297
  @impl NimblePool
298
  def terminate_worker(_reason, conn_or_template, config) do
178✔
299
    case conn_or_template do
178✔
300
      :template -> :ok
3✔
301
      conn -> Mint.HTTP1.close(conn)
175✔
302
    end
303

304
    {:ok, config}
305
  end
306

307
  defp connect({:template, scheme, host, port}, owner, deadline) do
308
    timeout = Ch.HTTP.to_timeout(deadline)
180✔
309

310
    case Mint.HTTP1.connect(scheme, host, port, mode: :passive, timeout: timeout) do
180✔
311
      {:ok, conn} ->
312
        case Mint.HTTP1.controlling_process(conn, owner) do
179✔
313
          {:ok, _conn} = ok ->
314
            ok
179✔
315

316
          {:error, _reason} = error ->
317
            Mint.HTTP1.close(conn)
×
318
            error
×
319
        end
320

321
      {:error, _reason} = error ->
322
        error
1✔
323
    end
324
  end
325

326
  defp connect({:ok, _conn} = ok, _owner, _deadline), do: ok
4,242✔
327

328
  defp request(conn, method, path, headers, body, deadline) do
329
    result =
4,421✔
330
      with {:ok, conn, _ref} <- Mint.HTTP1.request(conn, method, path, headers, body) do
×
331
        recv_all(conn, nil, [], nil, deadline)
4,421✔
332
      end
333

334
    with {:error, conn, reason} <- result do
4,421✔
335
      Mint.HTTP1.close(conn)
2✔
336
      {:error, reason}
337
    end
338
  end
339

340
  defp recv_all(conn, status, headers, data, deadline) do
341
    timeout = Ch.HTTP.to_timeout(deadline)
7,294✔
342

343
    case Mint.HTTP1.recv(conn, 0, timeout) do
7,294✔
344
      {:ok, conn, responses} ->
345
        case handle_responses(responses, status, headers, data) do
7,292✔
346
          {:ok, status, headers, data} -> {:ok, conn, status, headers, data}
4,419✔
347
          {:more, status, headers, data} -> recv_all(conn, status, headers, data, deadline)
2,873✔
348
          {:error, reason} -> {:error, conn, reason}
×
349
        end
350

351
      {:error, conn, reason, _responses} ->
352
        {:error, conn, reason}
2✔
353
    end
354
  end
355

356
  defp handle_responses([{:status, _ref, status} | rest], _prev_status = nil, headers, data) do
357
    handle_responses(rest, status, headers, data)
4,419✔
358
  end
359

360
  defp handle_responses([{:headers, _ref, new_headers} | rest], status, prev_headers, data) do
361
    handle_responses(rest, status, prev_headers ++ new_headers, data)
4,419✔
362
  end
363

364
  defp handle_responses([{:data, _ref, new_data} | rest], status, headers, prev_data) do
365
    next_data =
6,110✔
366
      case prev_data do
367
        nil -> new_data
3,038✔
368
        _ -> [prev_data | new_data]
3,072✔
369
      end
370

371
    handle_responses(rest, status, headers, next_data)
6,110✔
372
  end
373

374
  defp handle_responses([{:done, _ref}], status, headers, data) do
375
    {:ok, status, headers, data}
4,419✔
376
  end
377

378
  defp handle_responses([{:error, _ref, reason} | _rest], _status, _headers, _data) do
×
379
    {:error, reason}
380
  end
381

382
  defp handle_responses([], status, headers, data) do
383
    {:more, status, headers, data}
2,873✔
384
  end
385

386
  defp checkin(conn) do
387
    if Mint.HTTP1.open?(conn) do
4,419✔
388
      {:ok, conn}
389
    else
390
      {:remove, Mint.TransportError.exception(reason: :closed)}
391
    end
392
  end
393

394
  defp maybe_decompress(data, headers) do
395
    case get_header(headers, "content-encoding") do
3,035✔
396
      "zstd" when data != nil -> data |> IO.iodata_to_binary() |> :zstd.decompress()
3✔
397
      "gzip" when data != nil -> data |> IO.iodata_to_binary() |> :zlib.gunzip()
2✔
398
      nil -> data
3,030✔
399
      _ when data == nil -> data
×
400
      other -> raise "unsupported content encoding: #{inspect(other)}"
×
401
    end
402
  end
403

404
  defp decode_query_response(200, headers, body) do
405
    format = get_header(headers, "x-clickhouse-format")
4,399✔
406

407
    if format == "RowBinaryWithNamesAndTypes" do
4,399✔
408
      case body |> maybe_decompress(headers) |> response_body_to_binary() do
3,015✔
409
        "" ->
×
410
          {:ok, %Ch.Result{headers: headers, data: body}}
411

412
        decoded_data ->
413
          [names | rows] = Ch.RowBinary.decode_names_and_rows(decoded_data)
3,015✔
414

415
          {:ok,
416
           %Ch.Result{
417
             names: names,
418
             rows: rows,
419
             headers: headers,
420
             data: body
421
           }}
422
      end
423
    else
424
      {:ok, %Ch.Result{headers: headers, data: body}}
425
    end
426
  end
427

428
  defp decode_query_response(_status, headers, body) do
20✔
429
    message =
20✔
430
      body
431
      |> maybe_decompress(headers)
432
      |> response_body_to_binary()
433

434
    code =
20✔
NEW
435
      if code = get_header(headers, "x-clickhouse-exception-code") do
×
436
        String.to_integer(code)
20✔
437
      end
438

439
    {:error, %Ch.Error{code: code, message: message}}
440
  end
441

442
  defp response_body_to_binary(nil), do: ""
×
443
  defp response_body_to_binary(body), do: IO.iodata_to_binary(body)
3,035✔
444

445
  @compile inline: [get_header: 2]
446
  defp get_header(headers, name) do
447
    with {_, value} <- List.keyfind(headers, name, 0, nil), do: value
7,454✔
448
  end
449

450
  @compile inline: [put_new_header: 3]
451
  defp put_new_header(headers, name, value) do
452
    if List.keymember?(headers, name, 0) do
4,424✔
453
      headers
3✔
454
    else
455
      [{name, value} | headers]
456
    end
457
  end
458

459
  if Code.ensure_loaded?(Ecto.ParameterizedType) do
460
    @behaviour Ecto.ParameterizedType
461

462
    @impl Ecto.ParameterizedType
463
    def type(:string), do: :string
13✔
464
    def type(:boolean), do: :boolean
1✔
465
    def type(:uuid), do: Ecto.UUID
1✔
466
    def type(:date), do: :date
1✔
467
    def type(:date32), do: :date
1✔
468
    def type(:time), do: :time
1✔
469
    def type({:time64, _p}), do: :time
1✔
470
    def type(:datetime), do: :naive_datetime
1✔
471
    def type({:datetime, _tz}), do: :utc_datetime
11✔
472
    def type({:datetime64, _p}), do: :naive_datetime_usec
1✔
473
    def type({:datetime64, _p, _tz}), do: :utc_datetime_usec
2✔
474
    def type({:fixed_string, _s}), do: :string
1✔
475
    def type(:json), do: :map
1✔
476
    def type(:dynamic), do: :any
2✔
477

478
    for size <- [8, 16, 32, 64, 128, 256] do
479
      def type(unquote(:"i#{size}")), do: :integer
6✔
480
      def type(unquote(:"u#{size}")), do: :integer
10✔
481
    end
482

483
    for size <- [32, 64] do
484
      def type(unquote(:"f#{size}")), do: :float
2✔
485
    end
486

487
    def type({:decimal, _p, _s}), do: :decimal
1✔
488

489
    for size <- [32, 64, 128, 256] do
490
      def type({unquote(:"decimal#{size}"), _s}) do
4✔
491
        :decimal
492
      end
493
    end
494

495
    def type({:array, type}), do: {:array, type(type)}
2✔
496
    def type({:nullable, type}), do: type(type)
1✔
497
    def type({:low_cardinality, type}), do: type(type)
1✔
498
    def type({:simple_aggregate_function, _name, type}), do: type(type)
2✔
499
    def type(:ring), do: {:array, type(:point)}
5✔
500
    def type(:polygon), do: {:array, type(:ring)}
3✔
501
    def type(:multipolygon), do: {:array, type(:polygon)}
1✔
502
    def type({enum, _mappings}) when enum in [:enum8, :enum16], do: :any
2✔
503
    def type(:ipv4), do: :any
1✔
504
    def type(:ipv6), do: :any
1✔
505
    def type(:point), do: :any
7✔
506
    def type({:tuple, _types}), do: :any
1✔
507
    def type({:map, _key_type, _value_type}), do: :map
1✔
508
    def type({:variant, _types}), do: :any
1✔
509

510
    @impl Ecto.ParameterizedType
511
    def init(opts) do
512
      clickhouse_type =
56✔
513
        opts[:raw] || opts[:type] ||
56✔
514
          raise ArgumentError, "keys :raw or :type not found in: #{inspect(opts)}"
1✔
515

516
      Ch.Types.decode(clickhouse_type)
55✔
517
    end
518

519
    @impl Ecto.ParameterizedType
520
    def load(value, _loader, _params), do: {:ok, value}
55✔
521

522
    @impl Ecto.ParameterizedType
523
    def dump(value, _dumper, _params), do: {:ok, value}
66✔
524

525
    @impl Ecto.ParameterizedType
526
    def cast(value, :string = type), do: Ecto.Type.cast(type, value)
34✔
527
    def cast(value, :boolean = type), do: Ecto.Type.cast(type, value)
5✔
528
    def cast(value, :uuid), do: Ecto.Type.cast(Ecto.UUID, value)
3✔
529
    def cast(value, :date = type), do: Ecto.Type.cast(type, value)
4✔
530
    def cast(value, :date32), do: Ecto.Type.cast(:date, value)
4✔
531
    def cast(value, :time = type), do: Ecto.Type.cast(type, value)
4✔
532
    def cast(value, {:time64, _p}), do: Ecto.Type.cast(:time, value)
4✔
533
    def cast(value, :datetime), do: Ecto.Type.cast(:naive_datetime, value)
4✔
534
    def cast(value, {:datetime, _tz}), do: Ecto.Type.cast(:utc_datetime, value)
8✔
535
    def cast(value, {:datetime64, _p}), do: Ecto.Type.cast(:naive_datetime_usec, value)
4✔
536
    def cast(value, {:datetime64, _p, _tz}), do: Ecto.Type.cast(:utc_datetime_usec, value)
8✔
537
    def cast(value, {:fixed_string, _s}), do: Ecto.Type.cast(:string, value)
5✔
538
    def cast(value, :json), do: Ecto.Type.cast(:map, value)
7✔
539
    def cast(value, :dynamic), do: {:ok, value}
9✔
540

541
    for size <- [8, 16, 32, 64, 128, 256] do
542
      def cast(value, unquote(:"i#{size}")), do: Ecto.Type.cast(:integer, value)
26✔
543
      def cast(value, unquote(:"u#{size}")), do: Ecto.Type.cast(:integer, value)
28✔
544
    end
545

546
    for size <- [32, 64] do
547
      def cast(value, unquote(:"f#{size}")), do: Ecto.Type.cast(:float, value)
10✔
548
    end
549

550
    def cast(value, {:decimal = type, _p, _s}), do: Ecto.Type.cast(type, value)
1✔
551

552
    for size <- [32, 64, 128, 256] do
553
      def cast(value, {unquote(:"decimal#{size}"), _s}) do
554
        Ecto.Type.cast(:decimal, value)
4✔
555
      end
556
    end
557

558
    def cast(value, {:array, type}), do: Ecto.Type.cast({:array, type(type)}, value)
19✔
559
    def cast(value, {:nullable, type}), do: cast(value, type)
5✔
560
    def cast(value, {:low_cardinality, type}), do: cast(value, type)
5✔
561
    def cast(value, {:simple_aggregate_function, _name, type}), do: cast(value, type)
13✔
562

563
    def cast(value, :ring), do: Ecto.Type.cast({:array, type(:point)}, value)
1✔
564
    def cast(value, :polygon), do: Ecto.Type.cast({:array, type(:ring)}, value)
1✔
565
    def cast(value, :multipolygon), do: Ecto.Type.cast({:array, type(:polygon)}, value)
1✔
566

567
    def cast(nil, _params), do: {:ok, nil}
8✔
568

569
    def cast(value, {enum, mappings}) when enum in [:enum8, :enum16] do
570
      result =
14✔
571
        case value do
572
          _ when is_integer(value) -> List.keyfind(mappings, value, 1, :error)
4✔
573
          _ when is_binary(value) -> List.keyfind(mappings, value, 0, :error)
6✔
574
          _ -> :error
4✔
575
        end
576

577
      case result do
14✔
578
        {_, _} -> {:ok, value}
8✔
579
        :error = e -> e
6✔
580
      end
581
    end
582

583
    def cast(value, :ipv4) do
584
      case value do
6✔
585
        {a, b, c, d} when is_number(a) and is_number(b) and is_number(c) and is_number(d) ->
1✔
586
          {:ok, value}
587

588
        _ when is_binary(value) ->
589
          with {:error = e, _reason} <- :inet.parse_ipv4_address(to_charlist(value)), do: e
2✔
590

591
        _ when is_list(value) ->
592
          with {:error = e, _reason} <- :inet.parse_ipv4_address(value), do: e
2✔
593

594
        _ ->
1✔
595
          :error
596
      end
597
    end
598

599
    def cast(value, :ipv6) do
600
      case value do
8✔
601
        {a, s, d, f, g, h, j, k}
602
        when is_number(a) and is_number(s) and is_number(d) and is_number(f) and
603
               is_number(g) and is_number(h) and is_number(j) and is_number(k) ->
1✔
604
          {:ok, value}
605

606
        _ when is_binary(value) ->
607
          with {:error = e, _reason} <- :inet.parse_ipv6_address(to_charlist(value)), do: e
3✔
608

609
        _ when is_list(value) ->
610
          with {:error = e, _reason} <- :inet.parse_ipv6_address(value), do: e
3✔
611

612
        _ ->
1✔
613
          :error
614
      end
615
    end
616

617
    def cast(value, :point) do
618
      case value do
5✔
619
        {x, y} when is_number(x) and is_number(y) -> {:ok, value}
2✔
620
        _ -> :error
3✔
621
      end
622
    end
623

624
    def cast(value, {:tuple, types}), do: cast_tuple(types, value)
5✔
625
    def cast(value, {:map, key_type, value_type}), do: cast_map(value, key_type, value_type)
5✔
626
    def cast(value, {:variant, types}), do: cast_variant(types, value)
4✔
627

628
    defp cast_tuple(types, values) when is_tuple(values) do
629
      cast_tuple(types, Tuple.to_list(values), [])
3✔
630
    end
631

632
    defp cast_tuple(types, values) when is_list(values) do
633
      cast_tuple(types, values, [])
1✔
634
    end
635

636
    defp cast_tuple(_types, _values), do: :error
1✔
637

638
    defp cast_tuple([type | types], [value | values], acc) do
639
      case cast(value, type) do
6✔
640
        {:ok, value} -> cast_tuple(types, values, [value | acc])
5✔
641
        :error = e -> e
1✔
642
      end
643
    end
644

645
    defp cast_tuple([], [], acc), do: {:ok, List.to_tuple(:lists.reverse(acc))}
2✔
646
    defp cast_tuple(_types, _values, _acc), do: :error
1✔
647

648
    defp cast_map(value, key_type, value_type) when is_map(value) do
649
      cast_map(Map.to_list(value), key_type, value_type)
2✔
650
    end
651

652
    defp cast_map(value, key_type, value_type) when is_list(value) do
653
      cast_map(value, key_type, value_type, [])
4✔
654
    end
655

656
    defp cast_map(_value, _key_type, _value_type), do: :error
1✔
657

658
    defp cast_map([{key, value} | kvs], key_type, value_type, acc) do
659
      with {:ok, key} <- cast(key, key_type),
3✔
660
           {:ok, value} <- cast(value, value_type) do
2✔
661
        cast_map(kvs, key_type, value_type, [{key, value} | acc])
2✔
662
      end
663
    end
664

665
    defp cast_map([], _key_type, _value_type, acc), do: {:ok, Map.new(acc)}
2✔
666
    defp cast_map(_kvs, _key_type, _value_type, _acc), do: :error
1✔
667

668
    defp cast_variant([type | types], value) do
669
      case cast(value, type) do
9✔
670
        {:ok, _value} = ok -> ok
3✔
671
        :error -> cast_variant(types, value)
6✔
672
      end
673
    end
674

675
    defp cast_variant([], _value), do: :error
1✔
676

677
    @impl Ecto.ParameterizedType
678
    def embed_as(_, _), do: :self
×
679

680
    @impl Ecto.ParameterizedType
681
    def equal?(a, b, _), do: a == b
×
682

683
    @impl Ecto.ParameterizedType
684
    def format(params) do
685
      "#Ch<#{Ch.Types.encode(params)}>"
53✔
686
    end
687
  end
688
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc