Where does the changeset.changes come from in this example?

Consider the following code.

file lib/mango/crm/customer.ex (from Phoenix inside out, Mastering Phoenix Framework

defmodule Mango.CRM.Customer do
  use Ecto.Schema
  import Ecto.Changeset
  alias Mango.CRM.Customer
  import Comeonin.Bcrypt, only: [hashpwsalt: 1]

  schema "customers" do
    field :email, :string
    field :name, :string
    field :password, :string, virtual: true
    field :password_hash, :string
    field :phone, :string
    field :residence_area, :string


  @doc false
  def changeset(%Customer{} = customer, attrs) do
    |> cast(attrs, [:name, :email, :phone, :residence_area, :password])
    |> validate_required([:name, :email, :phone, :residence_area, :password])
    |> validate_format(:email, ~r/@/, message: "is invalid")
    |> validate_length(:password, min: 6, max: 100)
    |> unique_constraint(:email)
    |> put_hashed_password()

  defp put_hashed_password(changeset) do
    case changeset.valid? do
      true ->
        changes = changeset.changes
        put_change(changeset, :password_hash, hashpwsalt(changes.password))
      _ ->

When I hover the cursor over lowercase changeset inside put_hashed_password private function, it shows the following hover overlay
which means it’s the changeset function from the same file.

But I don’t see anything as changes in the changeset function, then where does this changeset.changes come from?

Whatever you use as IDE seems to be wrong here, as the changeset in your example refers to the local variable and not to the function.

1 Like

Thank you for the reply!

I’m using VSCode with ElixirLS.

As @NobbZ said, changeset is the local variable but the code editor is confused, because in Elixir function’s parenthesis are optional.
If you don’t wanna confuse the editor, write the function as follows,

  defp put_hashed_password(ch) do
    case ch.valid? do
      true ->
        changes = ch.changes
        put_change(ch, :password_hash, hashpwsalt(changes.password))
      _ ->
1 Like

Thank you @NobbZ and @DevotionGeo for the replies.

OK, it’s a local variable, I got that.

But I am still confused about the origin of the changes key. Where does this changes key come from?
Is the changes key present on every struct?

No, the changes key is present on only Ecto.Changeset struct.

In your terminal inside the project directory, run iex -S mix and then %Ecto.Changeset{},
you’ll get
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: nil, valid?: false>

(notice the changes: key with %{} value).

1 Like

Reading your code again, you’re actually passing the customer which is an Ecto.Changeset struct conforming to the %Customer{} struct, while matched as %Customer{} = customer (argument in the changeset function).
It means this changeset will allow only the keys specified in the Mango.CRM.Customer{} struct (which you aliased as alias Mango.CRM.Customer) and some other default keys, like changes, errors, valid? etc.

In that case if you write the put_hashed_password as follows, it will be clearer and less confusing, both for you and the code editor.

  defp put_hashed_password(customer) do
    case customer.valid? do
      true ->
        changes = customer.changes
        put_change(customer, :password_hash, hashpwsalt(changes.password))
      _ ->
1 Like

Thank you!
Now it makes much sense. I knew how to use it, but didn’t know how it worked under the hood.

1 Like

You’re welcome!

1 Like