Dropping tables in a safe way in Ruby on Rails

Abhay V Ashokan

By Abhay V Ashokan

on September 17, 2024

We are building NeetoCal which is a Calendly alternative. Recently we deployed the latest code to production. The code change involved deleting a table.

During the deployment, we noticed that some users got errors with status code 500 for a few minutes. This happened because the migration to drop the tables ran quickly and the tables got deleted. However, the old code was still referring to those tables.

strong_migrations didn't protect dropping of table

At NeetoCal, we are using the strong_migrations gem to catch unsafe migrations. The gem catches unsafe migration like removing a column but it doesn't capture unsafe operations like dropping a table.

Upon some digging, we found this issue where the author of the gem expressed unwillingness to add drop_table as an unsafe operation.

No worries. We can add dropping of a table as an unsafe operation in strong_migration ourselves. Here's how it can be done.

1# config/initializers/strong_migrations.rb
2
3StrongMigrations.add_check do |method, args|
4  if method == :drop_table
5    stop! "Dropping tables via migrations is discouraged."
6  end
7end

However, this will stop us from dropping tables using migration. Now the question is how should we drop tables since migrations will not allow us.

If we go with the approach of preventing the dropping of the table in migrations, then after the code deployment, we need to do some manual operations to drop the table. This involves keeping track of when to drop which table after which deployment. If a new person joins the project then that person wouldn't know when to drop the table since migrations can't be used to drop tables. Overall this solution brings other issues and hence this solution was a "no go" from our side.

Sam wants to delay dropping of tables and columns

Sam Saffron had ran into similar problems. He came up with a solution and he wrote about it in this blog.

His solution was not to drop the tables and columns immediately. Instead use "defer drops" to drop column or tables at least 30 minutes after the particular migration was run.

He introduced ColumnDropper and TableDropper to get this work done.

We felt that this solution adds an extra layer of complexity and we rejected this solution. Infact later we found that they ran into some issues with "defer drops" as discussed here.

Dropping should be allowed if it follows a pattern

After some internal discussion, we concluded that dropping tables needs to be allowed via migration but the names of tables and columns should explicitly make it clear that they are ready to be dropped.

The first thing we decided was that the dropping of the table or renaming column should happen over multiple deployments. In the first phase, tables and columns will be renamed. For example, table users should be renamed to users-deprecated-on-2024-07-09. This will ensure that no code is using the table. Now deploy the code.

In the second phase, we will actually delete the table or the column. In this phase, deletion will be allowed because the entity we are deleting is following a very specific naming pattern.

RuboCop rules to ensure the policy is followed

Now the task was to build a custom cop to enforce the policy.

1# bad
2drop_table :users
3
4# bad
5drop_table :users do |t|
6  t.string :email, null: false t.string
7  t.string :first_name, null: false
8end
9
10# good
11drop_table :users_deprecated_on_2024_08_09
12
13# good
14drop_table :users_deprecated_on_2024_08_09 do |t|
15  t.string :email, null: false
16  t.string :first_name, null: false
17end

We need to handle removal of column similarly.

1# bad
2remove_column :users, :email
3
4# bad
5change_table :users do |t|
6  t.remove :email
7end
8
9# good
10remove_column :users, :email_deprecated_on_2024_08_09
11
12# good
13drop_table :users do |t|
14  t.remove :email_deprecated_on_2024_08_09
15end

We added these two cops to our rubocop-neeto repo.

At Neeto, we ship changes at a fast pace. These cops add a layer of safety to our database operations to prevent unexpected issues in production. It also makes it easier to roll back the changes.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.