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

Add support for if_exists/if_not_exists on remove_column/add_column #38352

Merged
merged 1 commit into from Jan 30, 2020

Conversation

eileencodes
Copy link
Member

This PR adds support for if_exists on remove_column and
if_not_exists on add_column to support silently ignoring migrations
if the remove tries to remove a non-existent column or an add tries to
add an already existing column.

We (GitHub) have custom monkey-patched support for these features and
would like to upstream this behavior.

This matches the same behavior that is supported for create_table and
drop_table. The behavior for sqlite is different from mysql/postgres
and sqlite for remove column and that is reflected in the tests.

cc/ @rafaelfranca @tenderlove @jhawthorn

@eileencodes eileencodes added this to the 6.1.0 milestone Jan 30, 2020
@eileencodes eileencodes force-pushed the add-support-for-if-exists branch 2 times, most recently from 455ae9d to d93578c Compare January 30, 2020 14:01
Copy link
Contributor

@kaspth kaspth left a comment

Choose a reason for hiding this comment

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

Cool, my comments are more if there's some extra stuff we want to look into.

```
class AddColumnTitle < ActiveRecord::Migration[6.1]
def change
add_column :posts, :title, :string, if_not_exists: true
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we do something in the case where title exists, but it happens to be a text, then rolling back the existing column's removed and then migrating again, the column would now be a string? Though I suppose that's what you want and later migrations is the newest source of truth. Though it could be potentially confusing and happen silently.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good call. Turns out column_exists? takes a type. I've updated this to still raise if you pass a different type and added a test for that case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Woot 🙌

```
class RemoveColumnTitle < ActiveRecord::Migration[6.1]
def change
remove_column :posts, :title, if_exists: true
Copy link
Contributor

Choose a reason for hiding this comment

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

It's nice that both these options correspond to their SQL counterparts, but the sorta tailing if yet just an option reads odd to me. I find that I have to read both add_column and remove_column multiple times and sort of migrate/rollback the migration in my head. I wonder if there's something like this could work:

add_column    :posts, :title, :string, skip_when_migrated: true
remove_column :posts, :title, skip_when_migrated: true

I don't care so much that add_column requires if_not_exists while if_exists is only for remove_column, it's more about describing what happens in case the migration is needless. Maybe only_when_required: true or only_when_needed: true. Maybe suppress_error: true as another option.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm following the existing if_exists/if_not_exists that is used by create and drop table and I don't think they should be different because that's really confusing for users.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't love if_exists / if_not_exists but I definitely don't want create and drop to have one set of options and add/remove column to have totally different options. We could change the create/drop table but that creates a whole deprecation cycle.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah doh, I tried searching for if_not_exists on api.rubyonrails.org and came back blank. Sorry about the derailing, all good from here 🙏

This PR adds support for `if_exists` on `remove_column` and
`if_not_exists` on `add_column` to support silently ignoring migrations
if the remove tries to remove a non-existent column or an add tries to
add an already existing column.

We (GitHub) have custom monkey-patched support for these features and
would like to upstream this behavior.

This matches the same behavior that is supported for `create_table` and
`drop_table`. The behavior for sqlite is different from mysql/postgres
and sqlite for remove column and that is reflected in the tests.
def remove_column(table_name, column_name, type = nil, **options)
return if options[:if_exists] == true && !column_exists?(table_name, column_name)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we pass the type into the exists check here too?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about this and tried it out and there's an issue with passing type. For add_column the type is required, you have to pass it so we can use that to check if the column_exists?.

However, in remove_column it's allowed but completely ignored by the actual column dropper. For remove_column since type is ignored by the execute we don't need to pass type there because the column will be removed regardless of type so we don't want to protect on nil or type. Does that make sense? I feel like I'm rambling...

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically doesn't matter what you pass for type, remove column will always remove the column because type is ignored.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it, perfect 👌

@eileencodes eileencodes merged commit c280ae7 into rails:master Jan 30, 2020
@eileencodes eileencodes deleted the add-support-for-if-exists branch January 30, 2020 17:42
eileencodes added a commit to eileencodes/rails that referenced this pull request Apr 16, 2020
This PR allows for passing `if_exists` options to the `remove_index`
method so that we can ignore already removed indexes. This work follows
column `if/if_not_exists` from rails#38352 and `:if_not_exists` on `add_index`
from rails#38555.

We've found this useful at GitHub, there are migrations where we don't
want to raise if an index was already removed. This will allow us to
remove a monkey patch on `remove_index`.

I considered raising after the `index_name_for_remove` method is called
but that method will raise if the index doesn't exist before we get to
execute. I have a commit that refactors this but after much
consideration this change is cleaner and more straightforward than other
ways of implementing this.

This change also adds a little extra validation to the `add_index` test.
Fix `nodoc` on edited methods.
@slavadev
Copy link
Contributor

Hey,
I'm sorry for a late comment to a merged PR 😅 , but will it work properly with a rollback of the migration?

For example, I have:

def change
  add_column :posts, :title, :string, if_not_exists: true
end

During the initial migration, it will check if the column exists and creates a column.
But during the rollback, it will also check if the column exists(and it exists) and do nothing even though it should be deleted.

Most probably I'm missing something, but I can ask 🙂
Thanks!

@eileencodes
Copy link
Member Author

During the initial migration, it will check if the column exists and creates a column. But during the rollback, it will also check if the column exists(and it exists) and do nothing even though it should be deleted.

If you're using this you should define up and down because you'll likely want down to also consider whether your column exists.

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

Successfully merging this pull request may close these issues.

None yet

3 participants