Non-static default value in ecto schema

How to express a default value that changes over time in ecto schema?


schema "table" do
  field(:last_interaction, :utc_datetime, default: &DateTime.utc_now/0)
end

causes a runtime error

** (Ecto.ChangeError) value `&DateTime.utc_now/0` for `Resource.last_interaction` in `insert` does not match type :utc_datetime
    (ecto) lib/ecto/repo/schema.ex:789: Ecto.Repo.Schema.dump_field!/6
    (ecto) lib/ecto/repo/schema.ex:798: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
    (stdlib) lists.erl:1263: :lists.foldl/3
    (ecto) lib/ecto/repo/schema.ex:796: Ecto.Repo.Schema.dump_fields!/5
    (ecto) lib/ecto/repo/schema.ex:745: Ecto.Repo.Schema.dump_changes!/6
    (ecto) lib/ecto/repo/schema.ex:208: anonymous fn/14 in Ecto.Repo.Schema.do_insert/4
    (ecto) lib/ecto/repo/schema.ex:125: Ecto.Repo.Schema.insert!/4

schema "table" do
  field(:last_interaction, :utc_datetime, default: fn -> DateTime.utc_now() end)
end

doesn’t compile

== Compilation error in file .../resource.ex ==
** (ArgumentError) cannot escape #Function<1.89618492 in file:.../resource.ex>. The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, PIDs and remote functions in the format &Mod.fun/arity
    (elixir) src/elixir_quote.erl:114: :elixir_quote.argument_error/1
    (elixir) src/elixir_quote.erl:227: :elixir_quote.do_quote/3
    (elixir) src/elixir_quote.erl:402: :elixir_quote.do_splice/5
    (elixir) src/elixir_quote.erl:136: :elixir_quote.escape/2
    (elixir) lib/macro.ex:375: Macro.escape/2
    lib/ecto/schema.ex:1677: Ecto.Schema.__defstruct__/1
    .../resource.ex:19: (module)

schema "table" do
  field(:last_interaction, :utc_datetime, default: DateTime.utc_now())
end

returns the datetime of compilation.

3 Likes

This is not possible. Since the schema macro just compiles to a fancy form of defstruct and defstruct itself allows only literals in struct definition.

A way to solve this would be to use |> put_change(:last_interaction, DateTime.utc_now()) in the changeset function.

5 Likes

For this particular situation, a timestamp of the latest save, you could use the opts of Ecto.Schema.timestamps/1 like:

schema "table" do
  timestamps(inserted_at: false, updated_at: :last_interaction)
end

and Ecto will add it for you.

2 Likes

I’m not sure that every update to this record should change last_interaction field. It might have some “admin” fields that can be changed without affecting last_interaction date.

I actually just wanted it to default to DateTime.utc_now() on insert (and I already use timestamps()). And after that it were to be updated “manually”.

1 Like

I can understand that. Another alternative is to set up your migration so the db fills in a default for that field. Postgres has a CURRENT_TIMESTAMP function that can serve that purpose, though I’m not certain of the exact syntax in an Ecto.Migration to put a db expression as the default.

Note: if you use db defaults like this, you’ll probably want to set :read_after_writes on the field in your schema.

2 Likes