If you’ve ever worked on a Rails project with a team, you’ve probably run into an issue with changes appearing in db/schema.rb. The Rails robots that make up Active Record do their best to be helpful by keeping your db/schema.rb file up to date. But while they’re doing that, they tend to inject a bunch of other unwanted changes.
Here’s an example from a project I’ve been working on:
@@ -102,7 +102,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_06_230921) do
create_table "active_storage_attachments", force: :cascade do |t|
t.bigint "blob_id", null: false
- t.datetime "created_at", null: false
+ t.datetime "created_at", precision: nil, null: false
In this project, db/schema.rb has almost 4000 lines, and over 800 get automatically changed any time I run a migration. It’s a little annoying.
Not every change is exactly like the above example with precision. There are a few other small issues, but they all have the same root cause and fix.
It’s me, hi, I’m the root cause, it’s me.
The problem is that I’m lazy.
I know that the changes are because I’ve let my local database fall out of sync with what my local versions of Rails and Postgres work together to generate. In the case of these “precision” changes, it’s because the project has been upgraded to Rails 7 sometime after I created my database.
What can we do about it?
I could update my local database to include these precision constraints. But as you might guess, that comes with a few problems:
- I’d have to find every column that needs updating, and make that change
- Even if I did that, I’d run into other changes either caused by the Rails upgrade, or some other production change that didn’t affect my local database
- As mentioned above: I’m lazy
I could also completely drop my local database, and re-create it from scratch. Our team even has scripts to help with this. But I’m far lazier than the scripts are helpful. I just can’t be bothered.
Honestly my favrourite solution here is to ignore the problem: I’ll git checkout db/schema.rb
and move on with my life.
But sometimes that’s just not good enough. Just the other day, I needed to create a new migration.
My lazy self often wants to use git add -p
to select which changes I need, and continue to ignore the reset. But as I said earlier, I’ve got over 800 changes to sift through on this project. That’s too many for me to want to click through. The /
search option on git add -p
could help…but thankfully, laziness is a super-power; it led me to a better idea.
So what’s my weird trick? It’s just a plain-old git commit.
Step 1: Commit everything that we don’t want
Before adding my migration, I knew I’d run into this problem. What I wanted to do was capture the entire mess. So I triggered it by rolling back and re-running the most recent migration:
> git checkout main
> bin/rails db:rollback
> bin/rails db:migrate
Now, I have the complete set of changes in db/schema.rb that I don’t want to have to deal with. I can commit them:
git add db/schema.rb
git commit -m “Unwanted database changes”
Step 2: Commit the things we do want
At this point, I was feeling pretty good. I knew I could just pretend everything was fine, and my local database isn’t all kinds of messed up. It would all be OK.
I went ahead and did the plain-boring-old method for adding a migration:
> bin/rails generate migration AddPartNumberToWidgets part_number:string
invoke active_record
create db/migrate/20240320191332_add_part_number_to_widgets.rb
> bin/rails db:migrate
== 20240320191332 AddPartNumberToWidgets: migrating ===========================
-- add_column(:widgets, :part_number, :string)
== 20240320191332 AddPartNumberToWidgets: migrated (0.0058s)
> git commit -m "Add part_number to widgets"
[main 3b865550c] Add part_number to widgets
2 file changed, 6 insertions(+)
create mode 100644 db/migrate/20240320191332_add_part_number_to_widgets.rb
Step 3: Take out the trash, but keep the good stuff
Remember that first commit? We don’t need it. Let’s throw it out like the trash it is. The simplest/quickest way to do that was to:
> git reset --hard origin/main .
And now finally, I’d gotten to the magic bit. I had removed both commits, but the one I wanted was easy to bring back. All that’s needed is the SHA, which git commit had helpfully outputted earlier. With that, I could bring it back with a cherry-pick:
> git cherry-pick 3b865550c
And that’s it!
Now we have a single commit containing only our new changes, without any noise. It’s beautiful AND easy.