Weird behaviour with Ecto.Schema.embeds_one/2

I had a schema defined as below. Notice that I omitted the schema name for the embed.

defmodule MySchema do
schema "my_schema" do
 field :some_field, :string
 embeds_one :example_embed do
   field :test_field, :string
 end
end
end

This led to an error while casting the embed:

    ** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not an atom

        :erlang.apply([do: :ok], :__schema__, [:primary_key])
        (ecto 3.8.4) lib/ecto/changeset/relation.ex:116: Ecto.Changeset.Relation.cast/5
        (ecto 3.8.4) lib/ecto/changeset.ex:810: Ecto.Changeset.cast_relation/4

Another weird thing was that when I inspect the fields in my schema, I find that the fields in the embeded schema are now part of the main schema:

%MySchema{
some_field: nil,
example_embed: nil,
test_field: nil
}

The correct way to do it is to have the embed schema name, like:

schema "my_schema" do
 field :some_field, :string
 embeds_one :example_embed, Example do
   field :test_field, :string
 end
end

Don’t you think this should give a warning? :thinking:

You forgot to add schema name

defmodule MySchema do
  schema "my_schema" do
    field :some_field, :string
    embeds_one :example_embed, Example do
      field :test_field, :string
    end
  end
end

Yes, exactly. But don’t you think it should have given a warning?

Well, it states exactly what is wrong. Second argument must be an atom (like Example), but it is a do block

Yeah, but I was wishing for something like you haven't specified a name for embedded schema during compilation

Also it defined the fields in the parent schema, which is awkward

It does not need to be so…

This is because your declaration ultimately invokes this branch of embeds_many:

with schema bound to the AST for a kwlist containing one option (do) and opts bound to [].

expand_alias doesn’t find the AST shape it’s looking for, so it returns schema unchanged.

The code in quote expands to (roughly):

Ecto.Schema.__embeds_one__(MySchema, :example_embed, [do: field(:test_field, :string)], [])

Before calling __embeds_one__, the arguments are evaluated, so field(:test_field, :string) runs in the context of the schema’s block - that’s why the field appears on the schema.

field always returns :ok, so __embeds_one__ is called with a “schema” that’s a keyword list [do: :ok] which cast_relation crashes on.

3 Likes

This makes a lot of sense now.