自定义 DateRange Ecto 类型的转换错误
Conversion error with a custom DateRange Ecto type
我在编写自定义 Ecto 类型时遇到问题。它由 %Postgrex.Range{}
类型支持。
密码是
defmodule Foo.Ecto.DateRange do
@behaviour Ecto.Type
def type, do: :daterange
def cast(%{"lower" => lower, "upper" => upper}) do
new_lower = Date.from_iso8601! lower
new_upper = Date.from_iso8601! upper
{:ok, Date.range(new_lower, new_upper)}
end
def cast(%Date.Range{}=range) do
{:ok, range}
end
def cast(_), do: :error
def load(%Postgrex.Range{lower: lower, upper: upper}) do
{:ok, Date.range(lower, upper)}
end
def load(_), do: :error
def dump(%Date.Range{}=range) do
{:ok, %Postgrex.Range{lower: range.first, upper: range.last}}
end
def dump(_), do: :error
end
迁移是
def change do
create table(:users) do
add :email, :string, null: false
add :username, :string
add :name, :string, null: false
add :password_hash, :text, null: false
add :period, :daterange
timestamps()
end
用户架构是
schema "users" do
field :username, :string
field :name, :string
field :email, :string
field :password_hash, :string
field :password, :string, virtual: true
field :period, Foo.Ecto.DateRange
我的 seeds.exs
中有问题的代码是这个:
today = Date.utc_today()
{:ok, user2} = create_user %{name: "Gloubi Boulga",
email: "gloub@boul.ga", password: "xptdr32POD?é23PRK*efz",
period: Date.range(today, Timex.shift(today, months: 2))
}
最后,错误是这个:
* (CaseClauseError) no case clause matching: {~D[2017-11-04]}
(ecto) lib/ecto/adapters/postgres/datetime.ex:40: Ecto.Adapters.Postgres.TypeModule.encode_value/2
(ecto) /home/tchoutri/dev/Projects/Foo/deps/postgrex/lib/postgrex/type_module.ex:717: Ecto.Adapters.Postgres.TypeModule.encode_params/3
[…]
priv/repo/seeds.exs:33: anonymous fn/0 in :elixir_compiler_1.__FILE__/1
当然,我不明白为什么会发生这种转换,这非常令人沮丧,尤其是考虑到创建由 [=17 支持的自定义 Ecto 类型时=] 应该有点微不足道。
编辑: 我在转换函数中放了一些 Logger.debug
,我可以看到
[debug] Casting new_date #DateRange<~D[2017-11-11], ~D[2018-01-11]>
出现和
%Postgrex.Range{lower: ~D[2017-11-11], lower_inclusive: true, upper: ~D[2018-01-11], upper_inclusive: true}
在 dump
函数中。
在 %Postgrex.Range{}
内,当前版本的 Postgrex (0.13.3) 需要 %Postgrex.Date{}
s。见相关测试here.
然而,正如在 link 中看到的那样,%Postgrex.Date{}
在下一个版本中已被弃用,您应该从 0.14 开始使用 %Date{}
(仍在开发中)。
我今天遇到了这个。我希望这仍然有帮助:
def dump(%Date.Range{} = range) do
{:ok, %Postgrex.Range{lower: Date.to_erl(range.first), upper: Date.to_erl(range.last)}}
end
这是我最终得到的结果:
defmodule DateRange do
@moduledoc false
@behaviour Ecto.Type
@doc """
Does use the `:tsrange` postgrex type.
"""
def type, do: :daterange
@doc """
Can cast various formats:
# Simple maps (default to `[]` semantic like Date.range)
%{"lower" => "2015-01-23", "upper" => "2015-01-23"}
# Postgrex range with Date structs for upper and lower bound
%Postgrex.Range{lower: #Date<2015-01-23>, upper: #Date<2015-01-23>}
"""
def cast(%Date.Range{first: lower, last: upper}), do: cast(%{lower: lower, up
per: upper})
def cast(%{"lower" => lower, "upper" => upper}), do: cast(%{lower: lower, uppe
r: upper})
def cast(%Postgrex.Range{lower: %Date{}, upper: %Date{}} = range), do: {:ok, r
ange}
def cast(%{lower: %Date{} = lower, upper: %Date{} = upper}) do
{:ok, %Postgrex.Range{lower: lower, upper: upper}}
end
def cast(%{lower: lower, upper: upper}) do
try do
with {:ok, new_lower, 0} <- Date.from_iso8601(lower),
{:ok, new_upper, 0} <- Date.from_iso8601(upper) do
{:ok, %Postgrex.Range{lower: new_lower, upper: new_upper}}
else
_ -> :error
end
rescue
FunctionClauseError -> :error
end
end
def cast(_), do: :error
@end_of_times ~D[9999-12-31]
@start_of_times ~D[0000-01-01]
defp canonicalize_bounds(date, inclusive, offset, infinite_bound) do
with {:ok, date} <- Date.from_erl(date) do
case inclusive do
false -> {:ok, Timex.shift(date, days: offset)}
true -> {:ok, date}
end
else
^inclusive = false when is_nil(date) -> {:ok, infinite_bound}
_ -> :error
end
end
@doc """
Does load the postgrex returned range and converts data back to Date structs.
"""
def load(%Postgrex.Range{lower: lower, lower_inclusive: lower_inclusive,
upper: upper, upper_inclusive: upper_inclusive}) do
with {:ok, lower} <- canonicalize_bounds(lower, lower_inclusive, 1, @start_
of_times),
{:ok, upper} <- canonicalize_bounds(upper, upper_inclusive, -1, @end_of
_times) do
{:ok, Date.range(lower, upper)}
else
_ -> :error
end
end
def load(_), do: :error
@doc """
Does convert the Date bounds into erl format for the db.
"""
def dump(%Postgrex.Range{lower: %Date{} = lower, upper: %Date{} = upper} = range) do
with {:ok, lower} <- Ecto.DataType.dump(lower),
{:ok, upper} <- Ecto.DataType.dump(upper) do
{:ok, %{range | lower: lower, upper: upper}}
else
_ -> :error
end
end
def dump(_), do: :error
end
我在编写自定义 Ecto 类型时遇到问题。它由 %Postgrex.Range{}
类型支持。
密码是
defmodule Foo.Ecto.DateRange do
@behaviour Ecto.Type
def type, do: :daterange
def cast(%{"lower" => lower, "upper" => upper}) do
new_lower = Date.from_iso8601! lower
new_upper = Date.from_iso8601! upper
{:ok, Date.range(new_lower, new_upper)}
end
def cast(%Date.Range{}=range) do
{:ok, range}
end
def cast(_), do: :error
def load(%Postgrex.Range{lower: lower, upper: upper}) do
{:ok, Date.range(lower, upper)}
end
def load(_), do: :error
def dump(%Date.Range{}=range) do
{:ok, %Postgrex.Range{lower: range.first, upper: range.last}}
end
def dump(_), do: :error
end
迁移是
def change do
create table(:users) do
add :email, :string, null: false
add :username, :string
add :name, :string, null: false
add :password_hash, :text, null: false
add :period, :daterange
timestamps()
end
用户架构是
schema "users" do
field :username, :string
field :name, :string
field :email, :string
field :password_hash, :string
field :password, :string, virtual: true
field :period, Foo.Ecto.DateRange
我的 seeds.exs
中有问题的代码是这个:
today = Date.utc_today()
{:ok, user2} = create_user %{name: "Gloubi Boulga",
email: "gloub@boul.ga", password: "xptdr32POD?é23PRK*efz",
period: Date.range(today, Timex.shift(today, months: 2))
}
最后,错误是这个:
* (CaseClauseError) no case clause matching: {~D[2017-11-04]}
(ecto) lib/ecto/adapters/postgres/datetime.ex:40: Ecto.Adapters.Postgres.TypeModule.encode_value/2
(ecto) /home/tchoutri/dev/Projects/Foo/deps/postgrex/lib/postgrex/type_module.ex:717: Ecto.Adapters.Postgres.TypeModule.encode_params/3
[…]
priv/repo/seeds.exs:33: anonymous fn/0 in :elixir_compiler_1.__FILE__/1
当然,我不明白为什么会发生这种转换,这非常令人沮丧,尤其是考虑到创建由 [=17 支持的自定义 Ecto 类型时=] 应该有点微不足道。
编辑: 我在转换函数中放了一些 Logger.debug
,我可以看到
[debug] Casting new_date #DateRange<~D[2017-11-11], ~D[2018-01-11]>
出现和
%Postgrex.Range{lower: ~D[2017-11-11], lower_inclusive: true, upper: ~D[2018-01-11], upper_inclusive: true}
在 dump
函数中。
在 %Postgrex.Range{}
内,当前版本的 Postgrex (0.13.3) 需要 %Postgrex.Date{}
s。见相关测试here.
然而,正如在 link 中看到的那样,%Postgrex.Date{}
在下一个版本中已被弃用,您应该从 0.14 开始使用 %Date{}
(仍在开发中)。
我今天遇到了这个。我希望这仍然有帮助:
def dump(%Date.Range{} = range) do
{:ok, %Postgrex.Range{lower: Date.to_erl(range.first), upper: Date.to_erl(range.last)}}
end
这是我最终得到的结果:
defmodule DateRange do
@moduledoc false
@behaviour Ecto.Type
@doc """
Does use the `:tsrange` postgrex type.
"""
def type, do: :daterange
@doc """
Can cast various formats:
# Simple maps (default to `[]` semantic like Date.range)
%{"lower" => "2015-01-23", "upper" => "2015-01-23"}
# Postgrex range with Date structs for upper and lower bound
%Postgrex.Range{lower: #Date<2015-01-23>, upper: #Date<2015-01-23>}
"""
def cast(%Date.Range{first: lower, last: upper}), do: cast(%{lower: lower, up
per: upper})
def cast(%{"lower" => lower, "upper" => upper}), do: cast(%{lower: lower, uppe
r: upper})
def cast(%Postgrex.Range{lower: %Date{}, upper: %Date{}} = range), do: {:ok, r
ange}
def cast(%{lower: %Date{} = lower, upper: %Date{} = upper}) do
{:ok, %Postgrex.Range{lower: lower, upper: upper}}
end
def cast(%{lower: lower, upper: upper}) do
try do
with {:ok, new_lower, 0} <- Date.from_iso8601(lower),
{:ok, new_upper, 0} <- Date.from_iso8601(upper) do
{:ok, %Postgrex.Range{lower: new_lower, upper: new_upper}}
else
_ -> :error
end
rescue
FunctionClauseError -> :error
end
end
def cast(_), do: :error
@end_of_times ~D[9999-12-31]
@start_of_times ~D[0000-01-01]
defp canonicalize_bounds(date, inclusive, offset, infinite_bound) do
with {:ok, date} <- Date.from_erl(date) do
case inclusive do
false -> {:ok, Timex.shift(date, days: offset)}
true -> {:ok, date}
end
else
^inclusive = false when is_nil(date) -> {:ok, infinite_bound}
_ -> :error
end
end
@doc """
Does load the postgrex returned range and converts data back to Date structs.
"""
def load(%Postgrex.Range{lower: lower, lower_inclusive: lower_inclusive,
upper: upper, upper_inclusive: upper_inclusive}) do
with {:ok, lower} <- canonicalize_bounds(lower, lower_inclusive, 1, @start_
of_times),
{:ok, upper} <- canonicalize_bounds(upper, upper_inclusive, -1, @end_of
_times) do
{:ok, Date.range(lower, upper)}
else
_ -> :error
end
end
def load(_), do: :error
@doc """
Does convert the Date bounds into erl format for the db.
"""
def dump(%Postgrex.Range{lower: %Date{} = lower, upper: %Date{} = upper} = range) do
with {:ok, lower} <- Ecto.DataType.dump(lower),
{:ok, upper} <- Ecto.DataType.dump(upper) do
{:ok, %{range | lower: lower, upper: upper}}
else
_ -> :error
end
end
def dump(_), do: :error
end