Is it possible to dump custom types from Ecto.Schema without database interaction?

I’m currently trying to make a small integration layer between Ecto.Schema and mongodb_driver. So far I’ve managed to make it work, but I got stuck when dealing with custom types, namely they don’t transform when applying the changeset.

Let’s say I have the following setup:

defmodule TestSchema do
  use Ecto.Schema

  import Ecto.Changeset

  embedded_schema do
    field(:custom, Ecto.Enum, values: [test: 1, other: 2])
  end

  def changeset(%__MODULE__{} = schema, attrs) do
    schema
    |> cast(attrs, [:custom])
    |> validate_required([:custom])
  end
end

I’m using apply_action!/2 to transform the data from a changeset to a valid structure. The issue is that I get:

> TestSchema.changeset(%TestSchema{}, %{custom: :test}) |> apply_action!(:operation)
%TestSchema{id: nil, custom: :test}

When what I would expect is:

%TestSchema{id: nil, custom: 1}

I’ve tried to play with the embed_as option provided by Ecto.Enum but it doesn’t seem to make any difference.

Is there something I am missing to make this work?

PS: I have control over the dump process, so I can write functionality to dump/load these custom types, but it feels like ecto should do this by default.

Just a random guess: combine embed_as: :dumped with Ecto.Changeset.apply_changes/1 instead?

If that does not work – very likely will not – then I am afraid you’ll have to use Ecto.Enum.mappings/2 and do the mapping yourself. :confused:

2 Likes

That expectation is wrong. Ecto types deal with 3 representations of data.

  1. External data format (usually user input)
  2. Runtime data format
  3. Database storage format

The act of casting data in ecto takes data in 1. (or 2.) and transforms to 2. That is what you’re getting returned here expectedly. The act of applying a changeset only goes from changeset → plain schema struct. Schema structs generally hold values in the 2. format (the result of valid changes in changesets).

You want to look at Ecto.Type.dump / Ecto.Type.embedded_dump to go from 2. → 3.

5 Likes

This is exactly is what I tried at first, it doesn’t seem that that option does anything. apply_action/2 calls apply_changes/1 under the hood, so it’s basically the same function:

def apply_action(%Changeset{} = changeset, action) when is_atom(action) do
    if changeset.valid? do
      {:ok, apply_changes(changeset)}
    else
      {:error, %Changeset{changeset | action: action}}
    end
  end

OK, makes sense, but then it means that using ecto without a database implies that you will be missing a part of features, such as custom types, is that correct?

I find it quite strange that ecto types offer functionality for embedded dumps, but at the same time doesn’t offer a way to do that officially(without going into ecto internals and implementing it yourself), unless I am missing something, like a analogue to apply_changes/1 but which does the dump?

1 Like

You don’t need to use any ecto internals, you can combine Ecto’s (public) schema reflection with an Ecto.Type dump call on the field.

By the way here’s a diagram that I need to always refer back to when I want to remember how dump and load work:

2 Likes

Now I feel stupid for reaching for the mappings function! :smiley: