-
-
Notifications
You must be signed in to change notification settings - Fork 263
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
Rails/NotNullColumn: Why are defaults required? #237
Comments
Just for reference, here's the original PR: rubocop/rubocop#3415 |
@andyw8 Thank you! That helps a lot.
EDIT: I see that the big use case is when adding non-nullable columns to existing tables. In that regard, I think making the migration fail is much more useful than requiring a default value. That way, the user can decide how they want to treat existing records, whether that be by adding a default or a multi-step process of adding the column, determining/assigning the values, and then making the column non-nullable. Requiring a default value can have future consequences like my example in the original post about receiving missing data but saving successfully anyway. |
I agree with @oharace. Migrations can be difficult indeed, adding I believe there are (at least) two ways to introduce a "not-null-no-default" column.
I usually go with option 2, because the process does not require "wrong" defaults, and it allows me to easily identify "non-migrated" rows. Another thing that bothers me is that the documentation considers this example to be "good": add_reference :products, :category, null: false, default: 1 Linter-wise that example may make sense, but only if the target is to prevent the migration from crashing. EDIT: |
I'm probably going to restate everything in this thread. But I figured another example might be helpful. I was just working on a fresh Rails app. I generated a new migration using an example from https://guides.rubyonrails.org/active_record_migrations.html#writing-a-migration
This generates a new migration setting I understand the problem that's at play -- if there's existing rows, this migration won't succeed. In my case, I didn't have existing rows so the migration ran fine but Rubocop is mad (which was confusing). I think the worst part is the cop description (and a big complaint of mine regarding Rubocop in general). The description is: "Do not add a NOT NULL column without a default value.". That's incredibly unhelpful. A "why" would be much more helpful. Clearly the cop doesn't want me to add a not null column without a default, the cop is called "NotNullColumn" and it's erroring on this line. But WHY is that wrong? The answer to that question wasn't immediately obvious to me, especially since Rails generated this code. A description of "Do not add a NOT NULL column without a default value, as this will fail if there are existing rows. Set a default, or add the NOT NULL in a later migration after populating the column." would be much more helpful. There's a few other cops out there that have "what" descriptions without a "why", and it makes me wish there were some explanation of why a decision was made (is there some best practice I should know, instead of blindly following the cop?). It still feels really weird that I can run a Rails generator command straight from the docs and get a Rubocop failure. 🙁 |
Thank you for your replies, you both bring up valuable points. I think this cop provides a valuable warning, albeit with some rewording, but it shouldn't be forcing the user to set a default value. Instead, maybe it could provide a more-detailed message providing the user with some solutions such as:
Most cops aren't very wordy, though. Are there any cops you can think of that have a similar strategy like this? |
Could the cop ignore FK columns but work as-is for value columns? Unlike string/int value columns, FKs often don't have a good default to fallback to. Choosing a random record from the referenced table is often wrong. Choosing 1 or 0 to to satisfy the cop also doesn't solve anything in practice except to silence the cop. If the recommendation is to add a bogus value, that is as good as no cop. |
If you have data in the table where you're adding a reference you'll get an error like this:
You can fix the cop and run the migration by adding class AddCoveringToCoverRequests < ActiveRecord::Migration[7.0]
def change
add_reference :cover_requests, :covering, null: false, default: 1, foreign_key: { to_table: :employments }
change_column_default :cover_requests, :covering_id, from: 1, to: nil
end
end The case where you don't have any data in the table and you're adding a reference this cop is pretty annoying but I think the above is a reasonable solution. Keep the cop as it is useful for the other cases and use the above whenever adding foreign keys. |
@paulodeon The problem isn't that the cop exists, the problem is that the error message is not helpful.
That's not helpful. Tell the user why. I think that's my biggest complaint about a lot of cops. They don't explain a why, and so they just feel like an arbitrary set of rules. This one has a very clear why. So just say it. Here's an example that would make this cop much clearer. I'm sure it could be wordsmithed better:
Furthermore, adding a default is not always desirable. There's other ways to add a NOT NULL column without a default value. Strong Migrations explains how to add a NOT NULL to an existing column. So if you're wanting to add a NOT NULL column that does not have a default you can, but it's a multi-step process. First you add the column as nullable with no default. Then you follow the steps here to make it be not nullable. Note, you will need to backfill existing rows before validating the check constraint: |
@natematykiewicz I do agree with you - especially on the broader point about rubocop The example was to help others in the same boat without the need to change rubocop 😅 |
[Fix #237] Improve documentation for Rails/NotNullColumn
Clarify that the cop is only for NOT NULL without a default, and only for existing tables. Also point users in the direction of how to add a NOT NULL column to an existing table.
I'd like to generate some discussion on why this cop is requiring defaults for not-nullable columns.
First:
The code that made this cop fail was pretty basic:
Requiring a default value for a non-nullable foreign key seems strange to me. The only scenario where I can see this being useful is in a structure where all child records should be using a base parent record except when the child record explicitly needs to use a different parent record.
A non-nullable foreign key shouldn't be required to have a default. From a data-integrity standpoint, it is very useful to prevent records from being saved if they lack the data we expect.
In the cop's other scenario where we are adding a non-nullable column that is not a foreign key, it is still useful to not require a default value. If the system needs the column to have a default value, then by all means, add a default! But it shouldn't be required.
To help further explain my thought process, let's say we need to add a new column that keeps track of a user's name. We make this column,
:name
, non-nullable. For our business purposes, we do not want to default it. What would we default it to anyway, an empty string? The application should handle the validation and make sure the user provides us their name. In the off-chance that something happens and that insert/update (without a value for their name) reaches the DB, we want the insert/update to fail. Requiring the column to have a default would allow this record to still be saved, and now we have a data-integrity issue since the record is missing data we expect or need. Should the application have caught it? Yes. But the database is the source of truth and a default shouldn't always be required. (If the:name
column is non-nullable and has a default, is it truly required? Doesn't seem so.)I'd like to see what people think about:
I'm here to learn, and I'm keen to see your answers. Thanks for discussing!
The text was updated successfully, but these errors were encountered: