Do not reflect changes ignored due to :writable in returned struct#4736
Do not reflect changes ignored due to :writable in returned struct#4736green-david wants to merge 6 commits into
Conversation
|
Wait do I understand correctly that before we were not removing the fields that should be dropped? I'm really confused why our old tests passed then |
|
Oh I guess the old tests were only checking that we sent the correct fields to the write but not that we were dropping them correctly from the returned struct? |
Exactly! I didnt apply Jose's suggestion correctly in my last PR. We filtered them later before the write. |
There was a problem hiding this comment.
| test "update with on_writable_violation: :nothing saves changes for writable: :always and ignores changes for writable: :insert/:never" do | |
| %{never: nil} = | |
| %MySchemaWritable{id: 1} | |
| |> Ecto.Changeset.change(%{always: 10, never: 11, insert: 12}) | |
| |> TestRepo.update!() | |
| assert_received {:update, %{changes: [always: 10]}} | |
| end |
I think this is enough and then you can also make this minimal change to the existing insert test.
|
@greg-rychlewski I realized as well that even with this fix (and even prior to any of my changes I think) that non-writable changes are not reflected in the returned struct as mentioned in #4524, but that if a non-writable field has a value in the underlying data that is surfaced as a change during insert, the returned struct does reflect an updated value which I dont think should be happening? For example, this seems to fail: %{never: nil} =
%SchemaWritable{never: 10}
|> Ecto.Changeset.change(%{})
|> TestRepo.insert!()And this seems to succeed: %{never: nil} =
%SchemaWritable{}
|> Ecto.Changeset.change(%{never: 10})
|> TestRepo.insert!()I can try to come up with a fix later tonight. Perhaps we just force the underlying field in the data to nil, not sure. Would you prefer a separate PR for that specifically or put it in here? |
|
I think this PR is ok but I also have to think about it. This is kind of a weird case and I'm a bit wary of over-complicating the already complicated code. |
|
I guess |
There was a problem hiding this comment.
| changeset = update_in(changeset.changes, &drop_non_writable_changes!(&1, drop_fields, schema, :insert)) | |
| changeset = update_in(changeset.data, &best_way_to_nil_out_keys_in_elixir(&1, drop_fields)) |
@green-david something like this...i'm not sure off the top of my head if there is a built-in function to do this rather than rolling our own reducer but this is the idea
…accurately reflects skipped non-writable fields
…for non-writable fields
|
@greg-rychlewski @josevalim This appears to work, I think its ready for another look. 🙇♂️ |
| changes = | ||
| Enum.reduce(non_writable_fields, changeset.changes, fn field, changes -> | ||
| case changes do | ||
| %{^field => _change} -> | ||
| handle_writable_violation(field, schema, action) | ||
| Map.delete(changes, field) | ||
| %{} -> | ||
| changes | ||
| end | ||
| end) | ||
|
|
||
| # On insert, if the underlying struct has a value for a non-writable | ||
| # field, we need to nilify it so that the struct returned from the | ||
| # insert operation reflects that the write was not actually performed. | ||
| # Discarding the change above only prevents the actual write from happening. | ||
| data = case action do | ||
| :insert -> | ||
| updates = Enum.map(non_writable_fields, fn field -> {field, nil} end) | ||
| struct!(changeset.data, updates) | ||
| _ -> | ||
| changeset.data | ||
| end |
There was a problem hiding this comment.
| changes = | |
| Enum.reduce(non_writable_fields, changeset.changes, fn field, changes -> | |
| case changes do | |
| %{^field => _change} -> | |
| handle_writable_violation(field, schema, action) | |
| Map.delete(changes, field) | |
| %{} -> | |
| changes | |
| end | |
| end) | |
| # On insert, if the underlying struct has a value for a non-writable | |
| # field, we need to nilify it so that the struct returned from the | |
| # insert operation reflects that the write was not actually performed. | |
| # Discarding the change above only prevents the actual write from happening. | |
| data = case action do | |
| :insert -> | |
| updates = Enum.map(non_writable_fields, fn field -> {field, nil} end) | |
| struct!(changeset.data, updates) | |
| _ -> | |
| changeset.data | |
| end | |
| {changes, data_updates} = | |
| Enum.reduce(non_writable_fields, {changeset.changes, []}, fn field, {changes, data_updates} -> | |
| case changes do | |
| %{^field => _change} -> | |
| handle_writable_violation(field, schema, action) | |
| data_updates = if action == :insert, do: [{field, nil} | data_updates], else: data_updates | |
| {Map.delete(changes, field), data-updates} | |
| %{} -> | |
| {changes, data_updates} | |
| end | |
| end) | |
| data = if data_updates != [], do: struct!(changeset.data, data_updates), else: changeset.data |
maybe something like this...then you only iterate once and also you only update fields that were not nil to begin with
| %{always: 12, never: nil, insert: nil} = | ||
| %MySchemaWritableRaise{id: 3} | ||
| |> Ecto.Changeset.change(%{always: 12}) | ||
| |> TestRepo.update!() |
There was a problem hiding this comment.
I don't believe we need to change this since it is not trying to update things that shouldn't be updated and we have other tests for the happy path
| %MySchemaWritableRaise{id: 2} | ||
| |> Ecto.Changeset.change(%{insert: 11, always: 12}) | ||
| |> TestRepo.insert() | ||
| %{always: 12, never: nil, insert: 11} = |
There was a problem hiding this comment.
I believe we don't need this change either since we are not attempting to change something that shouldn't be changed and we have other tests already asserting the fields that should be changed are present
|
I am a bit unsure what to do in this case. It feels surfacing the field as changed, only to discard it, is a bit confusing. Perhaps what we whould do is to not surface it and then to a separate pass on insert on the struct for drop fields. So in a nutshell, we remove the |
Oops @greg-rychlewski, I realized just slightly too late that I accidentally broke #4524 / #4525 in #4734. This PR fixes the regression and adds accompanying unit tests.