Ecto insert Unix timestamp—validation error

I want to store a Unix timestamp with Ecto in a SQLite database. Unfortunately, I’m getting a changeset validation error when inserting timestamps e.g. 1643408628.

I’m new to Ecto so most likely it’s just some misunderstanding on my side…

Migration:

defmodule TelegramNotes.Repo.Migrations.CreateNotes do
  use Ecto.Migration

  def change do
    create table(:notes) do
      add :title, :string
      add :body, :text
      add :date, :utc_datetime

      timestamps()
    end
  end
end

Schema:

defmodule TelegramNotes.Notes.Note do
  use Ecto.Schema
  import Ecto.Changeset

  schema "notes" do
    field :title, :string
    field :body, :string
    field :date, :utc_datetime

    timestamps()
  end

  @doc false
  def changeset(note, attrs) do
    note
    |> cast(attrs, [:title, :body, :date])
    |> validate_required([:title, :date])
  end
end

After trying to insert something into the db I’m getting a changeset error:

** (MatchError) no match of right hand side value: {:error, #Ecto.Changeset<action: :insert, changes: %{title: "Hallo!"},
errors: [date: {"is invalid", [type: :utc_datetime, validation: :cast]}], data: #TelegramNotes.Notes.Note<>, valid?: false>}

Could anyone point me in the right direction?

Hey @r6203, from Ecto’s perspective, 1643408628 is an integer, not a datetime. If you want to convert that integer into a datetime you need to use DateTime — Elixir v1.13.2

3 Likes

Adding to @benwilson512’s answer, you might coerce the incoming value yourself because Ecto won’t do any coercion for you implicitly. Somewhat like

def changeset(note, attrs) do
  note
  |> cast(attrs, [:title, :body])
  |> validate_required([:title, :date])
  |> coerce_date()

where safe_cource_date/1 accepts a changeset and returns a valid changeset if the value can be coerced to DateTime instance.

defp coerce_date(%Changeset{changes: %{date: %DateTime{}}} = changeset),
  do: changeset
defp coerce_date(%Changeset{changes: %{date: date}} = changeset) when is_integer(date),
  do: change(changeset, %{date: DateTime.from_unix(date)})
defp coerce_date(%Changeset{changes: %{date: date}} = changeset) when is_binary(date),
  do: change(changeset, %{date: DateTime.from_iso8601(date)})
defp coerce_date(%Changeset{changes: %{date: date}} = changeset),
  do: %{changeset | errors: [{:date, ...} | changeset.errors], valid?: false}
2 Likes

Ecto won‘t accept non datetime changes in the first place. The best way to handle integer input is to use a custom ecto type, which converts integer to datetime on cast/1.

4 Likes

Indeed. One still can deal with it avoiding a call to cast/2 for this particular field, but I surely agree the custom Ecto type is better.

Thank you, all.

I’ve gone with a custom Ecto type. :+1: