How is action set for embedded changeset?

I noticed this behavior while trying to pattern match the :changes on a changeset with an embedded schema.

I have a simple changeset/2 function defined on an embedded schema for a Webhook:

defmodule Webhook do
  embedded_schema do
    field :webhook_id, :string
    field :topic, :string
  end

  def changeset(schema, changes) do
    Ecto.Changeset.cast(schema, changes, [:webhook_id, :topic])
  end
end

Which exists on a parent Account schema:

defmodule Account do
  schema "accounts" do
    field :name, :string
    embeds_many :webhooks, Webhook
  end

  def for_insert(changes) do
    %__MODULE__{}
    |> Ecto.Changeset.cast(changes, [:name])
    |> Ecto.Changeset.cast_embed(:webhooks)
  end
end

When Webhook.changeset/2 is triggered from the cast_embed/3 in Account, action: :insert is automatically added to the embedded changeset in the returned :changes. When I call Webhook.changeset/2 directly, however, it returns action: :nil.

I assume the :action is somehow being determined by cast_embed/3 then, but I’m struggling to find exactly where that happens and / or what the reasoning is?

cast_embed does indeed add those actions, as it knows which embeds where available beforehand and it can therefore decide which elements are to be inserted, updated or deleted.

When running just the changeset this information is not available, so it can only be added by Repo calls, which know which action actually happened.

3 Likes

That makes sense! My main confusion is that there are no explicit Repo calls in my code or test code. Does cast_embed execute one behind the scenes, then?

Nope. cast_assoc and cast_embed work by the state of the data in the source struct (preload in case of assocs) not by what is actually in the db.

1 Like

Got it, thanks for elaborating :slightly_smiling_face: