Content-Length: 507598 | pFad | http://github.com/electric-sql/electric/pull/2828/files

02 elixir-client: Add support for more Ecto field types by magnetised · Pull Request #2828 · electric-sql/electric · GitHub
Skip to content

elixir-client: Add support for more Ecto field types #2828

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-singers-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@core/elixir-client": patch
---

Add support for casting array and map fields to the Ecto.Adapter
5 changes: 5 additions & 0 deletions packages/elixir-client/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ if Mix.env() == :test do
report_file: "test-junit-report.xml",
automatic_create_dir?: true,
report_dir: "./junit"

config :plug, :statuses, %{
530 => "CF unable to resolve origen hostname",
599 => "Some random error"
}
end

config :electric,
Expand Down
27 changes: 24 additions & 3 deletions packages/elixir-client/lib/electric/client/ecto_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ if Code.ensure_loaded?(Ecto) do
@moduledoc false

alias Electric.Client.ShapeDefinition
alias Electric.Client.EctoAdapter.ArrayDecoder

@behaviour Electric.Client.ValueMapper

Expand Down Expand Up @@ -164,7 +165,8 @@ if Code.ensure_loaded?(Ecto) do
)
end

defp cast_to(:boolean) do
@doc false
def cast_to(:boolean) do
fn
nil -> nil
"t" -> true
Expand All @@ -173,7 +175,7 @@ if Code.ensure_loaded?(Ecto) do
end
end

defp cast_to({:parameterized, {_, _}} = type) do
def cast_to({:parameterized, {_, _}} = type) do
fn
nil ->
nil
Expand All @@ -192,7 +194,14 @@ if Code.ensure_loaded?(Ecto) do
end
end

defp cast_to(type) when is_atom(type) do
def cast_to({:array, type}) do
fn
nil -> nil
value -> ArrayDecoder.decode!(value, type)
end
end

def cast_to(type) when is_atom(type) do
with {:module, _} <- Code.ensure_loaded(type),
true <- function_exported?(type, :type, 0),
true <- function_exported?(type, :load, 1) do
Expand All @@ -207,6 +216,18 @@ if Code.ensure_loaded?(Ecto) do

defp cast_ecto(_type, nil), do: nil

defp cast_ecto(:map, value) do
Jason.decode!(value)
end

defp cast_ecto(:bitstring, value) when is_binary(value) do
<<String.to_integer(value, 2)::size(byte_size(value))>>
end

defp cast_ecto(:binary, "\\x" <> value) when is_binary(value) do
Base.decode16!(value, case: :lower)
end

defp cast_ecto(type, value) do
Ecto.Type.cast!(type, value)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule Electric.Client.EctoAdapter.ArrayDecoder do
alias Electric.Client.EctoAdapter

def decode!("{}", _type), do: []

def decode!(encoded_array, type) when is_binary(encoded_array) do
{"", [result]} =
decode_array(encoded_array, [], {EctoAdapter.cast_to(type), type, encoded_array})

result
end

##############################

defp decode_array("", acc, _state) do
{"", :lists.reverse(acc)}
end

defp decode_array(<<"{}", rest::bitstring>>, acc, state) do
decode_array(rest, [[] | acc], state)
end

defp decode_array(<<"{", rest::bitstring>>, acc, state) do
{rest, array} = decode_array(rest, [], state)
decode_array(rest, [array | acc], state)
end

defp decode_array(<<"}", rest::bitstring>>, acc, _state) do
{rest, :lists.reverse(acc)}
end

defp decode_array(<<?,, rest::bitstring>>, acc, state) do
decode_array(rest, acc, state)
end

defp decode_array(<<?", rest::bitstring>>, acc, state) do
{rest, elem} = decode_quoted_elem(rest, [], state)
decode_array(rest, [elem | acc], state)
end

defp decode_array(rest, acc, state) do
{rest, elem} = decode_elem(rest, [], state)
decode_array(rest, [elem | acc], state)
end

##############################

defp decode_elem(<<",", _::bitstring>> = rest, acc, state),
do: {rest, cast(acc, state)}

defp decode_elem(<<"}", _::bitstring>> = rest, acc, state),
do: {rest, cast(acc, state)}

defp decode_elem(<<c::utf8, rest::bitstring>>, acc, state),
do: decode_elem(rest, [acc | <<c::utf8>>], state)

defp decode_elem("", _acc, {_cast_fun, type, source}) do
raise Ecto.CastError, type: {:array, type}, value: source
end

##############################

defp decode_quoted_elem(<<?", rest::bitstring>>, acc, state),
do: {rest, cast_quoted(acc, state)}

defp decode_quoted_elem(<<"\\\"", rest::bitstring>>, acc, state),
do: decode_quoted_elem(rest, [acc | [?"]], state)

defp decode_quoted_elem(<<c::utf8, rest::bitstring>>, acc, state),
do: decode_quoted_elem(rest, [acc | <<c::utf8>>], state)

defp decode_quoted_elem("", _acc, {_cast_fun, type, source}) do
raise Ecto.CastError, type: {:array, type}, value: source
end

##############################

defp cast(iodata, {cast_fun, _type, _source}) do
iodata
|> IO.iodata_to_binary()
|> case do
"NULL" -> nil
value -> cast_fun.(value)
end
end

defp cast_quoted(iodata, {cast_fun, _type, _source}) do
iodata
|> IO.iodata_to_binary()
|> cast_fun.()
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule Electric.Client.EctoAdapter.ArrayDecoderTest do
use ExUnit.Case, async: true
alias Electric.Client.EctoAdapter.ArrayDecoder

test "simple string arrays" do
assert ArrayDecoder.decode!(~s[{}], :string) == []
assert ArrayDecoder.decode!(~s[{a,b,c}], :string) == ["a", "b", "c"]
assert ArrayDecoder.decode!(~s[{"a","b","c"}], :string) == ["a", "b", "c"]
assert ArrayDecoder.decode!(~s[{"a{}","b","c"}], :string) == ["a{}", "b", "c"]
assert ArrayDecoder.decode!(~S[{"a\"","b","c"}], :string) == ["a\"", "b", "c"]

assert ArrayDecoder.decode!(~S[{"this isn't here","b","c"}], :string) == [
"this isn't here",
"b",
"c"
]

assert ArrayDecoder.decode!(~S[{NULL,NULL,"NULL"}], :string) == [nil, nil, "NULL"]
end

test "nested string arrays" do
assert ArrayDecoder.decode!(~s[{{},{}}], :string) == [[], []]

assert ArrayDecoder.decode!(~s[{{"a","b","c"},{},{c,d,e}], :string) == [
["a", "b", "c"],
[],
["c", "d", "e"]
]
end

test "simple integer arrays" do
assert ArrayDecoder.decode!(~s[{}], :integer) == []
assert ArrayDecoder.decode!(~s[{1,2,3}], :integer) == [1, 2, 3]
assert ArrayDecoder.decode!(~S[{NULL,NULL,23}], :integer) == [nil, nil, 23]
end

test "nested integer arrays" do
assert ArrayDecoder.decode!(~s[{{},{}}], :integer) == [[], []]

assert ArrayDecoder.decode!(~s[{{1,2,3},{},{4,5,6}], :integer) == [
[1, 2, 3],
[],
[4, 5, 6]
]

assert ArrayDecoder.decode!(~s[{{{1,{2,{7,8}},3},{},{4,5,6}}], :integer) == [
[
[1, [2, [7, 8]], 3],
[],
[4, 5, 6]
]
]
end

test "nested jsonb arrays" do
assert ArrayDecoder.decode!(~s[{{{"{\\\"this\\\":4}"},"{}"},{},{"{}","{}"}], :map) == [
[[%{"this" => 4}], %{}],
[],
[%{}, %{}]
]
end

test "raises if the array is invalid" do
assert_raise Ecto.CastError, fn ->
ArrayDecoder.decode!(~s[{a,b,c"], :string)
end

assert_raise Ecto.CastError, fn ->
ArrayDecoder.decode!(~s[{a,b,"], :string)
end
end
end
Loading
Loading








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/electric-sql/electric/pull/2828/files

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy