What are the square brackets in my Rails migrations?

Let's look at what these little square brackets are doing in our Rails migration files

If you’ve worked on a Rails project in the last few years, you will likely have noticed some migration files have some square bracket syntax as part of their class name.

There are some cool things happening that I wanted to explain in this article. It certainly isn’t that common to see square brackets when calling a class name, so let’s dig into what is happening.

First things first, let’s talk about what is happening at a high level.

class MyAwesomeMigration < ActiveRecord::Migration[6.1]

The line above says, “We made this migration with Rails 6.1 in mind”.

This is important because if you’ve been working on a project for several years, the chances are you will have moved between at least one major version of Rails. Rails is such a lovely framework that it isn’t uncommon for people to never move away and they may end up with projects that they upgrade multiple times.

Migrations have got better over the years, and it stands to reason that a migration written with syntax meant for Rails 4 might not work in Rails 6.

The Rails team know you have enough on your plate migrating your application code between Rails versions, so they didn’t want you to have to go back and change the syntax on a load of old migration files too. This is where the version comes in.

If a version is specified, Rails will know what might need to be changed or patched to make the migration work correctly.

So, at a high level, if you see ActiveRecord::Migration[6.1], you know that Rails was running in version 6.1 at the point it made the migration.

The next question is, how did Rails get away with using square brackets as a class name? And doesn’t having loads of classes for each version of Rails get messy real quick?

The answer is to remember that in Ruby, everything is an object. When you define a class class Like < This, both Like and This don’t have to be classes; they just have to be something that evaluates to an instance of Class.

This means that if you wanted, you could write something like this;

class Like < This.lol
end

class This
  def self.lol
    TheActualClassName
  end
end

This would boil down to the class being class Like < TheActualClassName.

You might rightly be wondering, though, how they managed to get the square brackets in. This is some syntactic sugar that Ruby gives us.

Any class is allowed to respond to []; you’ve likely seen square brackets used on classes such as Array, Hash, and String, but custom classes can use them too.

That is what Rails does. It defines a class called migration with a method called def self.[](version).

This means whatever goes [inside here] will be in the version variable.

Here is the method in its one line of glory!

def self.[](version)
  Compatibility.find(version)
end

Compatibility.find can be found (ha ha) in the codebase here. It does some string manipulation, checks it is a real constant Rails should know about and returns it.

def self.find(version)
  version = version.to_s
  name = "V#{version.tr('.', '_')}"
  unless const_defined?(name)
    versions = constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete("V").tr("_", ".").inspect }
    raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}"
  end
  const_get(name)
end

In that file, you can even see how Rails checks for different versions to see how it should execute code differently depending on the version associated with the migration.

I think this is pretty neat!

This article is a part of the "Rails migrations" series

Recent posts View all

MacProductivity

Some Mac Tips

Some settings or tips I've learned over the years to make using your Mac an even nicer experience

Writing Git

How to speed up Rubocop

A small bit of config that could speed up your Rubocop runs