Skip to content

Add ability to configure the behavior of writing to a non-writable field#4734

Open
green-david wants to merge 2 commits into
elixir-ecto:masterfrom
green-david:dg-writable-violation-handling
Open

Add ability to configure the behavior of writing to a non-writable field#4734
green-david wants to merge 2 commits into
elixir-ecto:masterfrom
green-david:dg-writable-violation-handling

Conversation

@green-david

Copy link
Copy Markdown

This PR adds the ability to change the default behavior of silently ignoring changes to non-writable fields as discussed on the mailing list.

Attempting to write to a non-writable field can lead to code which appears to update the field but actually doesn't, leading to confusing behavior or hidden bugs. By allowing the user to specify that a warning should be logged or an exception raised, these errant writes can be detected and remedied.

Comment thread lib/ecto/repo/schema.ex Outdated
Comment on lines +641 to +650
message = "attempted to write to non-writable field #{inspect(name)} during #{action}"

case on_writable_violation do
:raise ->
raise ArgumentError, message
:warn ->
Logger.warning(message)
_ ->
:ok
end

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message here likely needs to be more robust and contain additional information, open to feedback. This is just a simple message for now to illustrate the functionality.

Comment thread lib/ecto/schema.ex
Comment on lines +704 to +709
* `:on_writable_violation` - Defines what action to take when performing an insert or update
attempts to modify a field that should not be modified according to it's `:writable` value.
Must be one of `:nothing`, `:warn`, or `:raise`. If set to `:nothing`, the modification is
silently ignored. If set to `:warn`, the modification is ignored and a warning is logged. If set
to `:raise`, an exception is raised and the operation is aborted. If `:writable` is set to `:always`,
`:on_writable_violation` must be set to `:nothing`. Defaults to `:nothing`.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this as "the path of least resistance" to illustrate the functionality, but I am not confident this is the desired way forward as to the public facing API of how to configure this behavior.

Some other options:

writable: [when: :never, on_violation: :raise]
writable: {:never, :raise}
writable: [:never, :raise]

Open to suggestions :)

@josevalim

Copy link
Copy Markdown
Member

Looking at the code, I like the on_writable_violation approach. However, I wonder if the implementation should be module.__schema__(:on_writable_violation). It should be much simpler and we only need to invoke it if there is a violation indeed.

@josevalim

Copy link
Copy Markdown
Member

WDYT?

@green-david

Copy link
Copy Markdown
Author

Looking at the code, I like the on_writable_violation approach. However, I wonder if the implementation should be module.__schema__(:on_writable_violation). It should be much simpler and we only need to invoke it if there is a violation indeed.

Sounds good, I will make this change 👍

@green-david

Copy link
Copy Markdown
Author

@josevalim I pushed a commit simplifying things a bit by using the __schema__(:on_writable_violation) approach you suggested. Let me know if that is what you had in mind👍

Would also like your thoughts on the warning and exception. It feels like the messages could be more useful by including possibly the schema name? It also seems like the warning could benefit from a stacktrace, but im not sure of the best way to go about that or if that is overkill / a potential performance killer.

I also considered a structured Ecto.WritableViolationError instead of a generic ArgumentError, but wasn't sure if that was overkill as well. Seems like it could be nice.

Thank you for the feedback as always!

Comment thread lib/ecto/repo/schema.ex
Comment on lines +630 to +636
case Map.pop(changes, field) do
{nil, changes} ->
changes
{_change, changes} ->
handle_writable_violation(field, schema, action)
changes
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can accidentally ignore a field being written to nil. The best is probably:

case changes do
  %{^field => _} ->
    handle_writable_violation(field, schema, action)
    Map.delete(changes, field)
  %{} ->
    changes

Comment thread lib/ecto/schema.ex
Comment on lines +2081 to +2084
if writable == :always and on_writable_violation != :nothing do
raise ArgumentError, "on_writable_violation must be :nothing for always writable fields"
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need this check. And maybe users want to set on_writable_violation: :warn as a default somewhere and this would get in the way. :)

Comment thread lib/ecto/schema.ex
end

Module.put_attribute(mod, :ecto_fields, {name, {type, writable}})
Module.put_attribute(mod, :ecto_fields, {name, {type, {writable, on_writable_violation}}})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not need to change this now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants