diff --git a/bench/cast.exs b/bench/cast.exs new file mode 100644 index 0000000..10fc7a6 --- /dev/null +++ b/bench/cast.exs @@ -0,0 +1,19 @@ +string = Ecto.ParameterizedType.init(Ch, type: "String") +int64 = Ecto.ParameterizedType.init(Ch, type: "Int64") +nullable_string = Ecto.ParameterizedType.init(Ch, type: "Nullable(String)") +low_cardinality_string = Ecto.ParameterizedType.init(Ch, type: "LowCardinality(String)") +tuple = Ecto.ParameterizedType.init(Ch, type: "Tuple(String, Int64)") +map = Ecto.ParameterizedType.init(Ch, type: "Map(String, UInt64)") + +Benchee.run( + %{ + "String" => fn -> Ecto.Type.cast(string, "value") end, + "Int64" => fn -> Ecto.Type.cast(int64, 10) end, + "Nullable(String)" => fn -> Ecto.Type.cast(nullable_string, "value") end, + "LowCardinality(String)" => fn -> Ecto.Type.cast(low_cardinality_string, "value") end, + "Tuple(String, Int64)" => fn -> Ecto.Type.cast(tuple, {"value", 10}) end, + "Map(String, UInt64)" => fn -> Ecto.Type.cast(map, %{"value" => 10}) end + }, + measure_function_call_overhead: true + # profile_after: :eprof +) diff --git a/lib/ch.ex b/lib/ch.ex index 46e4e7f..6313c79 100644 --- a/lib/ch.ex +++ b/lib/ch.ex @@ -93,184 +93,158 @@ defmodule Ch do end @impl true - def load(value, _loader, {:tuple, _types}), do: {:ok, value} - def load(value, _loader, {:map, _key_type, _value_type}), do: {:ok, value} + def load(value, _loader, _params), do: {:ok, value} - for type <- [:ipv4, :ipv6, :point, :ring, :polygon, :multipolygon] do - def load(value, _loader, unquote(type)), do: {:ok, value} - end - - def load(value, _loader, params), do: Ecto.Type.load(base_type(params), value) + @impl true + def dump(value, _dumper, _params), do: {:ok, value} @impl true - def dump(value, _dumper, {:tuple, types}) do - process_tuple(types, value, &Ecto.Type.dump/2) - end + def cast(value, :string = type), do: Ecto.Type.cast(type, value) + def cast(value, :boolean = type), do: Ecto.Type.cast(type, value) + def cast(value, :uuid), do: Ecto.Type.cast(Ecto.UUID, value) + def cast(value, :date = type), do: Ecto.Type.cast(type, value) + def cast(value, :date32), do: Ecto.Type.cast(:date, value) + def cast(value, :datetime), do: Ecto.Type.cast(:naive_datetime, value) + def cast(value, {:datetime, "UTC"}), do: Ecto.Type.cast(:utc_datetime, value) + def cast(value, {:datetime64, _p}), do: Ecto.Type.cast(:naive_datetime_usec, value) + def cast(value, {:datetime64, _p, "UTC"}), do: Ecto.Type.cast(:utc_datetime_usec, value) + def cast(value, {:fixed_string, _s}), do: Ecto.Type.cast(:string, value) - def dump(value, _dumper, {:map, key_type, value_type}) do - process_map(value, key_type, value_type, &Ecto.Type.dump/2) + for size <- [8, 16, 32, 64, 128, 256] do + def cast(value, unquote(:"i#{size}")), do: Ecto.Type.cast(:integer, value) + def cast(value, unquote(:"u#{size}")), do: Ecto.Type.cast(:integer, value) end - def dump(value, _dumper, :ipv4) do - case value do - {_, _, _, _} -> {:ok, value} - nil -> {:ok, value} - _other -> :error - end + for size <- [32, 64] do + def cast(value, unquote(:"f#{size}")), do: Ecto.Type.cast(:float, value) end - def dump(value, _loader, :ipv6) do - case value do - {_, _, _, _, _, _, _, _} -> {:ok, value} - nil -> {:ok, value} - _other -> :error - end - end + def cast(value, {:decimal = type, _p, _s}), do: Ecto.Type.cast(type, value) - def dump(value, _loader, :point) do - case value do - {x, y} when is_number(x) and is_number(y) -> {:ok, value} - nil -> {:ok, value} - _other -> :error + for size <- [32, 64, 128, 256] do + def cast(value, {unquote(:"decimal#{size}"), _s}) do + Ecto.Type.cast(:decimal, value) end end - def dump(value, _dumper, params), do: Ecto.Type.dump(base_type(params), value) + def cast(value, {:array, _type} = array), do: Ecto.Type.cast(array, value) + def cast(value, {:nullable, type}), do: cast(value, type) + def cast(value, {:low_cardinality, type}), do: cast(value, type) + def cast(value, {:simple_aggregate_function, _name, type}), do: cast(value, type) - @impl true - def cast(value, {:tuple, types}) do - with {:ok, value} <- process_tuple(types, value, &Ecto.Type.cast/2) do - {:ok, List.to_tuple(value)} + def cast(value, :ring), do: Ecto.Type.cast({:array, type(:point)}, value) + def cast(value, :polygon), do: Ecto.Type.cast({:array, type(:ring)}, value) + def cast(value, :multipolygon), do: Ecto.Type.cast({:array, type(:polygon)}, value) + + def cast(nil, _params), do: {:ok, nil} + + def cast(value, {enum, mappings}) when enum in [:enum8, :enum16] do + result = + case value do + _ when is_integer(value) -> List.keyfind(mappings, value, 1, :error) + _ when is_binary(value) -> List.keyfind(mappings, value, 0, :error) + _ -> :error + end + + case result do + {_, _} -> {:ok, value} + :error = e -> e end end def cast(value, :ipv4) do case value do - {_, _, _, _} -> {:ok, value} - _ when is_binary(value) -> :inet.parse_ipv4_address(to_charlist(value)) - _ when is_list(value) -> :inet.parse_ipv4_address(value) - nil -> {:ok, value} - _ -> :error + {a, b, c, d} when is_number(a) and is_number(b) and is_number(c) and is_number(d) -> + {:ok, value} + + _ when is_binary(value) -> + with {:error = e, _reason} <- :inet.parse_ipv4_address(to_charlist(value)), do: e + + _ when is_list(value) -> + with {:error = e, _reason} <- :inet.parse_ipv4_address(value), do: e + + _ -> + :error end end def cast(value, :ipv6) do case value do - {_, _, _, _, _, _, _, _} -> {:ok, value} - _ when is_binary(value) -> :inet.parse_ipv6_address(to_charlist(value)) - _ when is_list(value) -> :inet.parse_ipv6_address(value) - nil -> {:ok, value} - _ -> :error + {a, s, d, f, g, h, j, k} + when is_number(a) and is_number(s) and is_number(d) and is_number(f) and + is_number(g) and is_number(h) and is_number(j) and is_number(k) -> + {:ok, value} + + _ when is_binary(value) -> + with {:error = e, _reason} <- :inet.parse_ipv6_address(to_charlist(value)), do: e + + _ when is_list(value) -> + with {:error = e, _reason} <- :inet.parse_ipv6_address(value), do: e + + _ -> + :error end end def cast(value, :point) do case value do {x, y} when is_number(x) and is_number(y) -> {:ok, value} - nil -> {:ok, value} _ -> :error end end - def cast(value, {:map, key_type, value_type}) do - with {:ok, value} <- process_map(value, key_type, value_type, &Ecto.Type.cast/2) do - {:ok, Map.new(value)} - end - end - - def cast(value, params), do: Ecto.Type.cast(base_type(params), value) - - @doc false - def base_type(type) - - def base_type(t) when t in [:string, :boolean, :date], do: t - def base_type(:date32), do: :date - def base_type(:datetime), do: :naive_datetime - def base_type(:uuid), do: Ecto.UUID + def cast(value, {:tuple, types}), do: cast_tuple(types, value) + def cast(value, {:map, key_type, value_type}), do: cast_map(value, key_type, value_type) - # TODO - def base_type({:enum8, _mappings}), do: :string - def base_type({:enum16, _mappings}), do: :string - - for size <- [8, 16, 32, 64, 128, 256] do - def base_type(unquote(:"i#{size}")), do: :integer - def base_type(unquote(:"u#{size}")), do: :integer + defp cast_tuple(types, values) when is_tuple(values) do + cast_tuple(types, Tuple.to_list(values), []) end - for size <- [32, 64] do - def base_type(unquote(:"f#{size}")), do: :float + defp cast_tuple(types, values) when is_list(values) do + cast_tuple(types, values, []) end - def base_type({:array = a, type}), do: {a, base_type(type)} - def base_type({:nullable, type}), do: base_type(type) - def base_type({:low_cardinality, type}), do: base_type(type) - def base_type({:simple_aggregate_function, _name, type}), do: base_type(type) - def base_type({:fixed_string, _size}), do: :string - def base_type({:datetime, "UTC"}), do: :utc_datetime - def base_type({:datetime64, _precision}), do: :naive_datetime_usec - def base_type({:datetime64, _precision, "UTC"}), do: :utc_datetime_usec - def base_type({:decimal = d, _precision, _scale}), do: d - - for size <- [32, 64, 128, 256] do - def base_type({unquote(:"decimal#{size}"), _scale}), do: :decimal - end + defp cast_tuple(_types, _values), do: :error - def base_type(:point = p), do: {:parameterized, Ch, p} - def base_type(:ring), do: {:array, base_type(:point)} - def base_type(:polygon), do: {:array, base_type(:ring)} - def base_type(:multipolygon), do: {:array, base_type(:polygon)} - - def base_type({:parameterized, Ch, params}), do: base_type(params) - - defp process_tuple(types, values, mapper) when is_tuple(values) do - process_tuple(types, Tuple.to_list(values), mapper, []) - end - - defp process_tuple(types, values, mapper) when is_list(values) do - process_tuple(types, values, mapper, []) - end - - defp process_tuple(_types, nil = value, _mapper), do: {:ok, value} - defp process_tuple(_types, _values, _napper), do: :error - - defp process_tuple([t | types], [v | values], mapper, acc) do - case mapper.(base_type(t), v) do - {:ok, v} -> process_tuple(types, values, mapper, [v | acc]) + defp cast_tuple([type | types], [value | values], acc) do + case cast(value, type) do + {:ok, value} -> cast_tuple(types, values, [value | acc]) :error = e -> e end end - defp process_tuple([], [], _mapper, acc), do: {:ok, :lists.reverse(acc)} - defp process_tuple(_types, _values, _mapper, _acc), do: :error + defp cast_tuple([], [], acc), do: {:ok, List.to_tuple(:lists.reverse(acc))} + defp cast_tuple(_types, _values, _acc), do: :error - defp process_map(value, key_type, value_type, mapper) when is_map(value) do - process_map(Map.to_list(value), key_type, value_type, mapper) + defp cast_map(value, key_type, value_type) when is_map(value) do + cast_map(Map.to_list(value), key_type, value_type) end - defp process_map(value, key_type, value_type, mapper) when is_list(value) do - process_map(value, base_type(key_type), base_type(value_type), mapper, []) + defp cast_map(value, key_type, value_type) when is_list(value) do + cast_map(value, key_type, value_type, []) end - defp process_map(nil = value, _key_type, _value_type, _mapper), do: {:ok, value} - - defp process_map(_value, _key_type, _value_type, _mapper), do: :error + defp cast_map(_value, _key_type, _value_type), do: :error - defp process_map([{k, v} | kvs], key_type, value_type, mapper, acc) do - with {:ok, k} <- mapper.(key_type, k), - {:ok, v} <- mapper.(value_type, v) do - process_map(kvs, key_type, value_type, mapper, [{k, v} | acc]) - else - :error = e -> e + defp cast_map([{key, value} | kvs], key_type, value_type, acc) do + with {:ok, key} <- cast(key, key_type), + {:ok, value} <- cast(value, value_type) do + cast_map(kvs, key_type, value_type, [{key, value} | acc]) end end - defp process_map([], _key_type, _value_type, _mapper, acc), do: {:ok, :lists.reverse(acc)} - defp process_map(_kvs, _key_type, _value_type, _mapper, _acc), do: :error + defp cast_map([], _key_type, _value_type, acc), do: {:ok, Map.new(acc)} + defp cast_map(_kvs, _key_type, _value_type, _acc), do: :error @impl true def embed_as(_, _), do: :self @impl true def equal?(a, b, _), do: a == b + + @impl true + def format(params) do + "#Ch<#{Ch.Types.encode(params)}>" + end end end diff --git a/test/ch/ecto_type_test.exs b/test/ch/ecto_type_test.exs index 0220a45..91fde41 100644 --- a/test/ch/ecto_type_test.exs +++ b/test/ch/ecto_type_test.exs @@ -1,13 +1,32 @@ defmodule Ch.EctoTypeTest do use ExUnit.Case, async: true + describe "init" do + test "no :type or :raw" do + assert_raise ArgumentError, fn -> Ecto.ParameterizedType.init(Ch, []) end + end + + test "with :type" do + assert {:parameterized, Ch, :string} = Ecto.ParameterizedType.init(Ch, type: "String") + end + + test "with :raw" do + assert {:parameterized, Ch, :string} = Ecto.ParameterizedType.init(Ch, raw: "String") + end + end + test "String" do assert {:parameterized, Ch, :string} = type = Ecto.ParameterizedType.init(Ch, type: "String") - assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :string + assert Ecto.Type.format(type) == "#Ch" assert {:ok, "something"} = Ecto.Type.cast(type, "something") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, :something) + assert :error = Ecto.Type.cast(type, ~c"something") + assert :error = Ecto.Type.cast(type, 123) + assert {:ok, "something"} = Ecto.Type.dump(type, "something") assert {:ok, "something"} = Ecto.Type.load(type, "something") end @@ -17,9 +36,15 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "Nullable(String)") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :string + assert Ecto.Type.format(type) == "#Ch" assert {:ok, "something"} = Ecto.Type.cast(type, "something") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, ~c"something") + assert :error = Ecto.Type.cast(type, :something) + assert :error = Ecto.Type.cast(type, 123) + assert {:ok, "something"} = Ecto.Type.dump(type, "something") assert {:ok, "something"} = Ecto.Type.load(type, "something") end @@ -29,9 +54,15 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "LowCardinality(String)") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :string + assert Ecto.Type.format(type) == "#Ch" assert {:ok, "something"} = Ecto.Type.cast(type, "something") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, :something) + assert :error = Ecto.Type.cast(type, ~c"something") + assert :error = Ecto.Type.cast(type, 123) + assert {:ok, "something"} = Ecto.Type.dump(type, "something") assert {:ok, "something"} = Ecto.Type.load(type, "something") end @@ -41,9 +72,37 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "Array(String)") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == {:array, :string} + assert Ecto.Type.format(type) == "#Ch" + + assert {:ok, ["something"]} = Ecto.Type.cast(type, ["something"]) + assert {:ok, []} = Ecto.Type.cast(type, []) + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, [~c"something"]) + assert :error = Ecto.Type.cast(type, [:something]) + assert :error = Ecto.Type.cast(type, [123]) + assert :error = Ecto.Type.cast(type, 123) + + assert {:ok, ["something"]} = Ecto.Type.dump(type, ["something"]) + assert {:ok, ["something"]} = Ecto.Type.load(type, ["something"]) + end + + test "{:array, String}" do + assert {:array, {:parameterized, Ch, :string}} = + type = {:array, Ecto.ParameterizedType.init(Ch, type: "String")} + + assert Ecto.Type.type(type) == type + assert Ecto.Type.format(type) == "{:array, #Ch}" assert {:ok, ["something"]} = Ecto.Type.cast(type, ["something"]) + assert {:ok, []} = Ecto.Type.cast(type, []) + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, [:something]) + assert :error = Ecto.Type.cast(type, [~c"something"]) + assert :error = Ecto.Type.cast(type, [123]) + assert :error = Ecto.Type.cast(type, 123) + assert {:ok, ["something"]} = Ecto.Type.dump(type, ["something"]) assert {:ok, ["something"]} = Ecto.Type.load(type, ["something"]) end @@ -53,19 +112,20 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "Tuple(String, Int64)") assert Ecto.Type.type(type) == type + assert Ecto.Type.format(type) == "#Ch" assert {:ok, {"something", 42}} = Ecto.Type.cast(type, {"something", 42}) assert {:ok, {"something", 42}} = Ecto.Type.cast(type, ["something", 42]) - assert {:ok, ["something", 42]} = Ecto.Type.dump(type, {"something", 42}) - assert {:ok, {"something", 42}} = Ecto.Type.load(type, {"something", 42}) + assert {:ok, nil} = Ecto.Type.cast(type, nil) - assert :error = Ecto.Type.cast(type, {"something"}) - assert :error = Ecto.Type.dump(type, {"something"}) + assert :error = Ecto.Type.cast(type, {42, "something"}) - assert :error = Ecto.Type.cast(type, {"something", 42, true}) - assert :error = Ecto.Type.dump(type, {"something", 42, true}) + assert {:ok, {"something", 42}} = Ecto.Type.dump(type, {"something", 42}) + assert {:ok, {"something", 42}} = Ecto.Type.load(type, {"something", 42}) end + # TODO check size? + # TODO casting from binary wouldn't work for large values of 128 and 256 sized ints for size <- [8, 16, 32, 64, 128, 256] do for {encoded, decoded} <- [{"Int#{size}", :"i#{size}"}, {"UInt#{size}", :"u#{size}"}] do test encoded do @@ -73,9 +133,13 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: unquote(encoded)) assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :integer + assert Ecto.Type.format(type) == "#Ch<#{unquote(encoded)}>" assert {:ok, 1} = Ecto.Type.cast(type, 1) + assert {:ok, 1} = Ecto.Type.cast(type, "1") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, 1} = Ecto.Type.dump(type, 1) assert {:ok, 1} = Ecto.Type.load(type, 1) end @@ -87,9 +151,12 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "Map(String, UInt64)") assert Ecto.Type.type(type) == type + assert Ecto.Type.format(type) == "#Ch" assert {:ok, %{"answer" => 42}} = Ecto.Type.cast(type, %{"answer" => 42}) - assert {:ok, [{"answer", 42}]} = Ecto.Type.dump(type, %{"answer" => 42}) + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert {:ok, %{"answer" => 42}} = Ecto.Type.dump(type, %{"answer" => 42}) assert {:ok, %{"answer" => 42}} = Ecto.Type.load(type, %{"answer" => 42}) end @@ -99,9 +166,15 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: unquote("Float#{size}")) assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :float + assert Ecto.Type.format(type) == "#Ch" assert {:ok, 1.0} = Ecto.Type.cast(type, 1.0) + assert {:ok, 1.0} = Ecto.Type.cast(type, 1) + assert {:ok, 1.0} = Ecto.Type.cast(type, "1.0") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, 1.0} = Ecto.Type.dump(type, 1.0) assert {:ok, 1.0} = Ecto.Type.load(type, 1.0) end @@ -111,10 +184,14 @@ defmodule Ch.EctoTypeTest do assert {:parameterized, Ch, :date} = type = Ecto.ParameterizedType.init(Ch, type: "Date") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :date + assert Ecto.Type.format(type) == "#Ch" assert {:ok, ~D[2001-01-01]} = Ecto.Type.cast(type, ~D[2001-01-01]) assert {:ok, ~D[2001-01-01]} = Ecto.Type.cast(type, "2001-01-01") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, ~D[2001-01-01]} = Ecto.Type.dump(type, ~D[2001-01-01]) assert {:ok, ~D[2001-01-01]} = Ecto.Type.load(type, ~D[2001-01-01]) end @@ -123,10 +200,14 @@ defmodule Ch.EctoTypeTest do assert {:parameterized, Ch, :date32} = type = Ecto.ParameterizedType.init(Ch, type: "Date32") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :date + assert Ecto.Type.format(type) == "#Ch" assert {:ok, ~D[2001-01-01]} = Ecto.Type.cast(type, ~D[2001-01-01]) assert {:ok, ~D[2001-01-01]} = Ecto.Type.cast(type, "2001-01-01") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, ~D[2001-01-01]} = Ecto.Type.dump(type, ~D[2001-01-01]) assert {:ok, ~D[2001-01-01]} = Ecto.Type.load(type, ~D[2001-01-01]) end @@ -135,10 +216,14 @@ defmodule Ch.EctoTypeTest do assert {:parameterized, Ch, :boolean} = type = Ecto.ParameterizedType.init(Ch, type: "Bool") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :boolean + assert Ecto.Type.format(type) == "#Ch" assert {:ok, true} = Ecto.Type.cast(type, true) assert {:ok, false} = Ecto.Type.cast(type, false) + assert {:ok, true} = Ecto.Type.cast(type, "true") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, true} = Ecto.Type.dump(type, true) assert {:ok, true} = Ecto.Type.load(type, true) end @@ -148,10 +233,14 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "DateTime") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :naive_datetime + assert Ecto.Type.format(type) == "#Ch" assert {:ok, ~N[2001-01-01 12:00:00]} = Ecto.Type.cast(type, ~N[2001-01-01 12:00:00]) assert {:ok, ~N[2001-01-01 12:00:00]} = Ecto.Type.cast(type, "2001-01-01 12:00:00") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, ~N[2001-01-01 12:00:00]} = Ecto.Type.dump(type, ~N[2001-01-01 12:00:00]) assert {:ok, ~N[2001-01-01 12:00:00]} = Ecto.Type.load(type, ~N[2001-01-01 12:00:00]) end @@ -161,20 +250,25 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "DateTime('UTC')") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :utc_datetime + assert Ecto.Type.format(type) == "#Ch" assert {:ok, ~U[2001-01-01 12:00:00Z]} = Ecto.Type.cast(type, ~U[2001-01-01 12:00:00Z]) assert {:ok, ~U[2001-01-01 12:00:00Z]} = Ecto.Type.cast(type, "2001-01-01 12:00:00Z") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, ~U[2001-01-01 12:00:00Z]} = Ecto.Type.dump(type, ~U[2001-01-01 12:00:00Z]) assert {:ok, ~U[2001-01-01 12:00:00Z]} = Ecto.Type.load(type, ~U[2001-01-01 12:00:00Z]) end + # TODO truncate? test "DateTime64(3)" do assert {:parameterized, Ch, {:datetime64, 3}} = type = Ecto.ParameterizedType.init(Ch, type: "DateTime64(3)") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :naive_datetime_usec + assert Ecto.Type.format(type) == "#Ch" assert {:ok, ~N[2001-01-01 12:00:00.123456]} = Ecto.Type.cast(type, ~N[2001-01-01 12:00:00.123456]) @@ -182,6 +276,10 @@ defmodule Ch.EctoTypeTest do assert {:ok, ~N[2001-01-01 12:00:00.123456]} = Ecto.Type.cast(type, "2001-01-01 12:00:00.123456") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, ~N[2001-01-01 12:00:00.123456]} = Ecto.Type.dump(type, ~N[2001-01-01 12:00:00.123456]) @@ -189,12 +287,13 @@ defmodule Ch.EctoTypeTest do Ecto.Type.load(type, ~N[2001-01-01 12:00:00.123456]) end + # TODO truncate? test "DateTime64(3, 'UTC')" do assert {:parameterized, Ch, {:datetime64, 3, "UTC"}} = type = Ecto.ParameterizedType.init(Ch, type: "DateTime64(3, 'UTC')") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :utc_datetime_usec + assert Ecto.Type.format(type) == "#Ch" assert {:ok, ~U[2001-01-01 12:00:00.123456Z]} = Ecto.Type.cast(type, ~U[2001-01-01 12:00:00.123456Z]) @@ -202,6 +301,10 @@ defmodule Ch.EctoTypeTest do assert {:ok, ~U[2001-01-01 12:00:00.123456Z]} = Ecto.Type.cast(type, "2001-01-01 12:00:00.123456Z") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "asdf") + assert {:ok, ~U[2001-01-01 12:00:00.123456Z]} = Ecto.Type.dump(type, ~U[2001-01-01 12:00:00.123456Z]) @@ -214,25 +317,39 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "SimpleAggregateFunction(any, String)") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :string + assert Ecto.Type.format(type) == "#Ch" assert {:ok, "something"} = Ecto.Type.cast(type, "something") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, :something) + assert :error = Ecto.Type.cast(type, ~c"something") + assert :error = Ecto.Type.cast(type, 123) + assert {:ok, "something"} = Ecto.Type.dump(type, "something") assert {:ok, "something"} = Ecto.Type.load(type, "something") end + # TODO check size? test "FixedString(3)" do assert {:parameterized, Ch, {:fixed_string, 3}} = type = Ecto.ParameterizedType.init(Ch, type: "FixedString(3)") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :string + assert Ecto.Type.format(type) == "#Ch" assert {:ok, "som"} = Ecto.Type.cast(type, "som") + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, ~c"som") + assert :error = Ecto.Type.cast(type, :som) + assert :error = Ecto.Type.cast(type, 123) + assert {:ok, "som"} = Ecto.Type.dump(type, "som") assert {:ok, "som"} = Ecto.Type.load(type, "som") end + # TODO Ecto.Enum options? for size <- [8, 16] do decoded = :"enum#{size}" encoded = "Enum#{size}" @@ -243,24 +360,27 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: unquote(full_encoded)) assert Ecto.Type.type(type) == type - - # TODO Ecto.Enum? - assert Ch.base_type(type) == :string + assert Ecto.Type.format(type) == "#Ch<#{unquote(full_encoded)}>" assert {:ok, "hello"} = Ecto.Type.cast(type, "hello") assert {:ok, "world"} = Ecto.Type.cast(type, "world") - # assert {:ok, "hello"} = Ecto.Type.cast(type, 1) - # assert {:ok, "world"} = Ecto.Type.cast(type, 2) + assert {:ok, 1} = Ecto.Type.cast(type, 1) + assert {:ok, 2} = Ecto.Type.cast(type, 2) + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "hi") + assert :error = Ecto.Type.cast(type, :hello) + assert :error = Ecto.Type.cast(type, :world) assert {:ok, "hello"} = Ecto.Type.dump(type, "hello") assert {:ok, "world"} = Ecto.Type.dump(type, "world") - # assert {:ok, 1} = Ecto.Type.dump(type, 1) - # assert {:ok, 2} = Ecto.Type.dump(type, 2) + assert {:ok, 1} = Ecto.Type.dump(type, 1) + assert {:ok, 2} = Ecto.Type.dump(type, 2) assert {:ok, "hello"} = Ecto.Type.load(type, "hello") assert {:ok, "world"} = Ecto.Type.load(type, "world") - # assert {:ok, "hello"} = Ecto.Type.load(type, 1) - # assert {:ok, "world"} = Ecto.Type.load(type, 2) + assert {:ok, 1} = Ecto.Type.load(type, 1) + assert {:ok, 2} = Ecto.Type.load(type, 2) end end @@ -268,25 +388,34 @@ defmodule Ch.EctoTypeTest do assert {:parameterized, Ch, :uuid} = type = Ecto.ParameterizedType.init(Ch, type: "UUID") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == Ecto.UUID + assert Ecto.Type.format(type) == "#Ch" uuid = Ecto.UUID.generate() bin_uuid = Ecto.UUID.dump!(uuid) assert {:ok, ^uuid} = Ecto.Type.cast(type, uuid) assert {:ok, ^uuid} = Ecto.Type.cast(type, bin_uuid) - assert {:ok, ^bin_uuid} = Ecto.Type.dump(type, uuid) - # assert {:ok, ^uuid} = Ecto.Type.load(type, uuid) - assert {:ok, ^uuid} = Ecto.Type.load(type, bin_uuid) + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert {:ok, ^uuid} = Ecto.Type.dump(type, uuid) + assert {:ok, ^bin_uuid} = Ecto.Type.load(type, bin_uuid) end test "IPv4" do assert {:parameterized, Ch, :ipv4} = type = Ecto.ParameterizedType.init(Ch, type: "IPv4") assert Ecto.Type.type(type) == type + assert Ecto.Type.format(type) == "#Ch" assert {:ok, {127, 0, 0, 1}} = Ecto.Type.cast(type, "127.0.0.1") + assert {:ok, {127, 0, 0, 1}} = Ecto.Type.cast(type, ~c"127.0.0.1") assert {:ok, {127, 0, 0, 1}} = Ecto.Type.cast(type, {127, 0, 0, 1}) + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, "::1") + assert :error = Ecto.Type.cast(type, ~c"::1") + assert :error = Ecto.Type.cast(type, 127) + assert {:ok, {127, 0, 0, 1}} = Ecto.Type.dump(type, {127, 0, 0, 1}) assert {:ok, {127, 0, 0, 1}} = Ecto.Type.load(type, {127, 0, 0, 1}) end @@ -295,9 +424,20 @@ defmodule Ch.EctoTypeTest do assert {:parameterized, Ch, :ipv6} = type = Ecto.ParameterizedType.init(Ch, type: "IPv6") assert Ecto.Type.type(type) == type + assert Ecto.Type.format(type) == "#Ch" assert {:ok, {0, 0, 0, 0, 0, 0, 0, 1}} = Ecto.Type.cast(type, "::1") + assert {:ok, {0, 0, 0, 0, 0, 0, 0, 1}} = Ecto.Type.cast(type, ~c"::1") assert {:ok, {0, 0, 0, 0, 0, 0, 0, 1}} = Ecto.Type.cast(type, {0, 0, 0, 0, 0, 0, 0, 1}) + assert {:ok, nil} = Ecto.Type.cast(type, nil) + + assert {:ok, {0, 0, 0, 0, 0, 65535, 32512, 1}} = Ecto.Type.cast(type, "127.0.0.1") + assert {:ok, {0, 0, 0, 0, 0, 65535, 32512, 1}} = Ecto.Type.cast(type, ~c"127.0.0.1") + + assert :error = Ecto.Type.cast(type, "abcd") + assert :error = Ecto.Type.cast(type, ~c"abcd") + assert :error = Ecto.Type.cast(type, 1) + assert {:ok, {0, 0, 0, 0, 0, 0, 0, 1}} = Ecto.Type.dump(type, {0, 0, 0, 0, 0, 0, 0, 1}) assert {:ok, {0, 0, 0, 0, 0, 0, 0, 1}} = Ecto.Type.load(type, {0, 0, 0, 0, 0, 0, 0, 1}) end @@ -306,18 +446,25 @@ defmodule Ch.EctoTypeTest do assert {:parameterized, Ch, :point} = type = Ecto.ParameterizedType.init(Ch, type: "Point") assert Ecto.Type.type(type) == type + assert Ecto.Type.format(type) == "#Ch" - point = {10, 10} - assert {:ok, point} == Ecto.Type.cast(type, point) - assert {:ok, point} == Ecto.Type.dump(type, point) - assert {:ok, point} == Ecto.Type.load(type, point) + assert {:ok, {10, 10}} == Ecto.Type.cast(type, {10, 10}) + assert {:ok, {11.2, 23.4}} == Ecto.Type.cast(type, {11.2, 23.4}) + assert {:ok, nil} == Ecto.Type.cast(type, nil) + + assert :error = Ecto.Type.cast(type, %{x: 10, y: 10}) + assert :error = Ecto.Type.cast(type, {"10", "10"}) + assert :error = Ecto.Type.cast(type, "(10,10)") + + assert {:ok, {10, 10}} == Ecto.Type.dump(type, {10, 10}) + assert {:ok, {10, 10}} == Ecto.Type.load(type, {10, 10}) end test "Ring" do assert {:parameterized, Ch, :ring} = type = Ecto.ParameterizedType.init(Ch, type: "Ring") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == {:array, {:parameterized, Ch, :point}} + assert Ecto.Type.format(type) == "#Ch" ring = [{0, 0}, {10, 0}, {10, 10}, {0, 10}] assert {:ok, ring} == Ecto.Type.cast(type, ring) @@ -330,7 +477,7 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "Polygon") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == {:array, {:array, {:parameterized, Ch, :point}}} + assert Ecto.Type.format(type) == "#Ch" polygon = [ [{20, 20}, {50, 20}, {50, 50}, {20, 50}], @@ -347,7 +494,7 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: "MultiPolygon") assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == {:array, {:array, {:array, {:parameterized, Ch, :point}}}} + assert Ecto.Type.format(type) == "#Ch" multipolygon = [ [[{0, 0}, {10, 0}, {10, 10}, {0, 10}]], @@ -364,11 +511,11 @@ defmodule Ch.EctoTypeTest do type = Ecto.ParameterizedType.init(Ch, type: unquote("Decimal(18, 4)")) assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :decimal + assert Ecto.Type.format(type) == "#Ch" assert {:ok, %Decimal{}} = Ecto.Type.cast(type, 1.0) - assert {:ok, %Decimal{}} = Ecto.Type.dump(type, 1.0) - assert {:ok, %Decimal{}} = Ecto.Type.load(type, 1.0) + assert {:ok, %Decimal{}} = Ecto.Type.dump(type, Decimal.new("1.0")) + assert {:ok, %Decimal{}} = Ecto.Type.load(type, Decimal.new("1.0")) end for size <- [32, 64, 128, 256] do @@ -376,12 +523,20 @@ defmodule Ch.EctoTypeTest do assert {:parameterized, Ch, {unquote(:"decimal#{size}"), 4}} = type = Ecto.ParameterizedType.init(Ch, type: unquote("Decimal#{size}(4)")) + precision = + case unquote(size) do + 32 -> 9 + 64 -> 18 + 128 -> 38 + 256 -> 76 + end + assert Ecto.Type.type(type) == type - assert Ch.base_type(type) == :decimal + assert Ecto.Type.format(type) == "#Ch" assert {:ok, %Decimal{}} = Ecto.Type.cast(type, 1.0) - assert {:ok, %Decimal{}} = Ecto.Type.dump(type, 1.0) - assert {:ok, %Decimal{}} = Ecto.Type.load(type, 1.0) + assert {:ok, %Decimal{}} = Ecto.Type.dump(type, Decimal.new("1.0")) + assert {:ok, %Decimal{}} = Ecto.Type.load(type, Decimal.new("1.0")) end end end