0

I'm having trouble with a custom Ecto type that I'm writing. It is be backed by %Postgrex.Range{} type. The code is

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

The migration is

  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

The user schema is

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

The problematic code in my seeds.exs is this one:

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))
}

And finally, the error is this one:

* (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

And of course, I do not understand why this kind of conversion is happening, and this is very frustrating, especially considering that creating a custom Ecto type backed by %Postgrex.Range{} should be somewhat trivial.

EDIT: I've put some Logger.debug in the cast function and I can see

[debug] Casting new_date #DateRange<~D[2017-11-11], ~D[2018-01-11]> 

appearing and

%Postgrex.Range{lower: ~D[2017-11-11], lower_inclusive: true, upper: ~D[2018-01-11], upper_inclusive: true}

in the dump function.

3 Answers3

0

Within a %Postgrex.Range{}, the current version of Postgrex (0.13.3) expects %Postgrex.Date{}s. See the relevant test here.

However as seen in the link, %Postgrex.Date{} is deprecated in the next release and you are expected to use %Date{} from 0.14 onwards (still in development).

Jay Jun
  • 221
  • 2
  • 6
0

I came across this today. I hope this still helps:

def dump(%Date.Range{} = range) do
  {:ok, %Postgrex.Range{lower: Date.to_erl(range.first), upper: Date.to_erl(range.last)}}
end
0

Here's what I ended up with:

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