Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow schema fields to be set to read only #4335

Merged
merged 13 commits into from
Dec 12, 2023

Conversation

greg-rychlewski
Copy link
Member

@greg-rychlewski greg-rychlewski commented Dec 12, 2023

Motivation

The :mode option will allow users to choose if a field should be read only or read-write. It is basically the same as adding a :read_only option only having a single field will let us add write only fields in the same place later on instead of creating a new field, if we want to add it.

Read only fields are useful for a few reasons:

  1. We recently added the generated option to migrations. This allows users to put them into their schemas and have them just work™.
  2. There are some workflows that benefit from having read only fields. For example, this option issue: on_confict: :replace_all gives ERROR 42601 (syntax_error) when using fields that source existing fields #4251. The user would like two copies of the same field in their schema.
  3. It solves part of the problems stated in this PR: Proposal: Derived fields #4261. Users can create generated fields in their migrations and utilize them in much the same way that was described in that issue. There are some limitations/differences but there is some overlap there.

Changes

There were a lot of cases to consider. I will list them below:

Casting

  • We allow casting read only fields the same way we do for virtual fields. The user might want to work with the changeset but not persist the field.

Insert

  • When surfacing the changes, we silently filter read only fields, same as virtual fields. This is really the only way it can work because all fields are surfaced on insert so it doesn't make sense to raise.
  • We want read only fields to be returned when returning: true is specified
  • We don't want read only fields to be updated in on_conflict. They will be silently ignored for replace_all because the user is not specifying them. For {:replace, fields} and conflict queries an error will be raised because the user is going out of their way to specify them.

Insert All

  • We raise immediately if a header contains a read only field. Both for lists of maps and insert queries.
  • Same behaviour for returning and on conflict applies here as it did for insert

Update

  • Same as insert, we silently ignore read only fields in the changeset. It might make sense to raise here but we don't do so for virtual fields. If we want to change one I think there's a case to change both.
  • Same behaviour as insert for returning. There is no on_conflict to worry about.

Update All

  • Raise if a read only field is given to the :update key in the query.

Select

  • The read only fields should be able to be selected
  • The read only fields should be part of selecting the entire struct
  • The read only fields should be allowed to be used as query parameters

Delete

  • Read only fields don't come into play here

Delete All

  • Same behaviour as select queries

@josevalim
Copy link
Member

having a single field will let us add write only fields in the same place later on instead of creating a new field

Do you think it makes sense to have write only fields? I am asking because we already use read_only elsewhere, so it would be nice to be consistent.

lib/ecto/schema.ex Outdated Show resolved Hide resolved
@greg-rychlewski
Copy link
Member Author

Do you think it makes sense to have write only fields? I am asking because we already use read_only elsewhere, so it would be nice to be consistent.

I actually started with read_only because I couldn't think of any use case for write only. And then I started doubting myself that maybe someone will come by with a valid use case later on.

To me it's a strange idea though and I never really encountered that behaviour anywhere. I'm happy to change back to read_only if you also think it's ok.

@josevalim
Copy link
Member

The implementation looks clean and concise. The addition of writeable_field is excellent to encapsulate them.

For now, I would prefer to call them read_only for consistency with everything else.

I also wonder if there is a relationship between the following options:

  1. virtual fields and read_only fields. It feels they are not related, given virtual fields are not read from repository.

  2. read_only fields and read_after_writes. It feels they are orthogonal. A field can be read only and may or may not be read after writes.

I am not saying there is, just wondering. Any other option we may have missed?

@greg-rychlewski
Copy link
Member Author

The other one is :load_in_query which makes sure that it is selected when the entire struct is selected. But similar to the other ones it doesn't quite cover the entire case. Because you can still write to these fields.

:virtual I don't believe is suitable here because we want :read_only fields to be read from the database.

The other field options are completely unrelated:

 :default,
    :source,
    :autogenerate,
    :read_after_writes,
    :virtual,
    :primary_key,
    :load_in_query,
    :redact,
    :foreign_key,
    :on_replace,
    :defaults,
    :type,
    :where,
    :references,
    :skip_default_validation,
    :mode

@greg-rychlewski
Copy link
Member Author

Virtual is pretty close because you could select an existing database field into it if you're explicit. But you would not be able to retrieve it by selecting something like from p in Post. And then another issue is you could not use :read_after_writes on virtual fields, which is what the user here wanted to do #4251

@josevalim
Copy link
Member

Yeah, read_only is actually configuring writes (i.e. writes are never allowed). Both load_in_query and read_after_writes are configured the reads so they could be unified, but I don't think it would be much better. For example:

  • read_on: [:reads, :writes], the default is :reads

There is at least the argument that load_in_query should have been called read_in_query but even that is a bit misleading (they can be read, they just are not loaded).

Similarly, we could have write_on: [:writes] and setting it to write_on: [] means read only. But yeah, I don't think any of the APIs proposed in this comment are better, so I think we are good to go!

@greg-rychlewski greg-rychlewski changed the title Add mode option to schema Allow schema fields to be set to read only Dec 12, 2023
Copy link
Member

@josevalim josevalim left a comment

Choose a reason for hiding this comment

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

Just one tiny comment and ship it!

lib/ecto/repo/schema.ex Outdated Show resolved Hide resolved
lib/ecto/repo/schema.ex Outdated Show resolved Hide resolved
lib/ecto/repo/schema.ex Outdated Show resolved Hide resolved
@greg-rychlewski
Copy link
Member Author

I forgot about embeds btw. Do you think we should raise if read_only is given as an option to a field in an embedded schema?

I was thinking yes because we cannot really merge the new stuff into the old value. We can only do a complete overwrite.

@josevalim
Copy link
Member

Do you think we should raise if read_only is given as an option to a field in an embedded schema?

Yes, it is a perfectly fine starting point.

@greg-rychlewski
Copy link
Member Author

@josevalim thanks for all the feedback. i think this is the cleanest iteration so far. let me know what you think!

Copy link
Member

@josevalim josevalim left a comment

Choose a reason for hiding this comment

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

Beauitiful, just minor nits!

@greg-rychlewski greg-rychlewski merged commit 253e2a3 into elixir-ecto:master Dec 12, 2023
6 checks passed
@greg-rychlewski greg-rychlewski deleted the read_only_fields branch December 12, 2023 19:53
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