Behavior of Ecto writable: :insert is confusing me

Ecto 3.12.4 on Elixir 1.17.3

After significant playing around with Ecto with fields defined with writable: :insert I am confused. Here is my scenario:

I had a field defined as writable: :insert which was because I wanted it to be set only on create and never changed. Unfortunately this was an issue for one particular use case I had. I won’t go into the use cases details because they are not relevant.

What is relevant is that the code with the field set to writable: :insert was returning from the update of that struct with the field set to the new value even though that new value was never persisted.

Code snippet

  case Mutate.create_client(altered_params) do
      {:ok, client1} ->
        IO.inspect(client1)

#--> %Sct.Network.Client{
  __meta__: #Ecto.Schema.Metadata<:loaded, "collectives">,
  id: 1603900,
  name: "aaa33",
  permalink: "2e3cdcfe-d082-4e4b-bb2a-469e35b63bf9",
  type: "Organization",
  master_client_id: 1603893,
  created_at: ~U[2024-10-28 21:33:16.077693Z],
  updated_at: ~U[2024-10-28 21:33:16.077693Z]
}

        # at this point the master_client_id has been set to a inserted value and clent1.master_client_id matches what is in database

        {:ok, client2} =
          Mutate.update_client(client1, %{
            master_client_id: client1.id
          })

#changeset => |> Client.changeset(Map.drop(attrs, [:id, :type, :eusage, :permalink])) #=> #Ecto.Changeset<
  action: nil,
  changes: %{master_client_id: 1603900},
  errors: [],
  data: #Sct.Network.Client<>,
  valid?: true,
  ...
>

   # this is where I get a bit confined. Client1.id (the input to my change) i
# s a different value then client1.master_client_id and with that 
# field set to writable: :insert this does not alter the value in the database. 
# That was expected (after I reviewed it) but the return result is 
# categorized as ":ok" HOWEVER client2 now shows as 
# having master_client_id ALTERED.  

#=> {:ok,
 %Sct.Network.Client{
   __meta__: #Ecto.Schema.Metadata<:loaded, "collectives">,
   id: 1603900,
   name: "aaa33",
   permalink: "2e3cdcfe-d082-4e4b-bb2a-469e35b63bf9",
   type: "Organization",
   master_client_id: 1603900,
   created_at: ~U[2024-10-28 21:33:16.077693Z],
   updated_at: ~U[2024-10-28 21:33:16.077693Z]
 }}

        IO.inspect(client2)

%Sct.Network.Client{
  __meta__: #Ecto.Schema.Metadata<:loaded, "collectives">,
  id: 1603900,
  name: "aaa33",
  master_client_id: 1603900,
  created_at: ~U[2024-10-28 21:33:16.077693Z],
  updated_at: ~U[2024-10-28 21:33:16.077693Z]
}

        true = client2.master_client_id == client2.id

        persisted_client = Sct.Client.Query.get_client!(client2.id)

        IO.inspect(persisted_client)

%Sct.Network.Client{
  __meta__: #Ecto.Schema.Metadata<:loaded, "collectives">,
  id: 1603900,
  name: "aaa33",
  master_client_id: 1603893,
  created_at: ~U[2024-10-28 21:33:16.077693Z],
  updated_at: ~U[2024-10-28 21:33:16.077693Z]
}

So I am either confused about why I am getting a changeset that says it is valid? when I am attempting to update an insert only field ( or )
I am confused about what an “:ok” returned value from an Ecto Update means when it has altered the struct but never persisted that alteration, in fact in this case NEVER made a DB request at all!!!

It seems like one or the other of these outcomes destroys trust in the responses from Ecto???

Help me resolve this confusion please.

FWIW, there’s a test case for writable specifically NOT making changesets invalid:

I would say that the changeset itself does not know your intent, will this become an insert or update or just a form validation, so it cannot return an error.
For the call to update, I think 2 things matter, first if the value hasn’t changed ecto will never attempt to write to the database, and secondly if that value shouldn’t be written to the database and no other fields have changed what then should be in the action to the database.

I have never used :writable, so I may be completely of the mark.

I could be convinced pretty easily that the changeset itself should not be invalid. I am less convinced that the update - when provided a different value on a non-writable field - should return with :ok AND the non-writable field altered in the returned struct. This is based on my inference that the returned struct should represent the struct as UPDATED in the DB.

This may be related: `field/3`'s `:writable` seems to ignore the `:insert` option · Issue #4524 · elixir-ecto/ecto · GitHub

1 Like

yes @josevalim I believe you are correct. I will pause until this code is released and then re-assess.

Thanks to all for the discussion.