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.