Class: Ippon::Migrator
- Inherits:
-
Object
- Object
- Ippon::Migrator
- Defined in:
- lib/ippon/migrator.rb
Overview
Migrator provides a migration system on top of / Sequel. It should be trivial to add if you’re already using Sequel to access your database, but you can also use it to migrate a database you’re primarily accessing through other means.
Migrator provides a way to write migrations in a single file (no generators required) and you can integrate it in any system in minutes. As your application grows, it provides ways to structure migrations in multiple files.
Getting started
To get started created a file called migrate.rb
and populate it:
require 'ippon/migrator'
require 'something_that_connects_to_your_database'
db = Sequel::DATABASES.last # if you don't have access to it directly
m = Ippon::Migrator.new(db)
m.migrate "1-initial-schema" do |db|
db.create_table(:users) do
primary_key :id
Text :email, null: false, unique: true
Text :crypted_password
end
end
m.print_summary
m.apply
You can now run it once to migrate,
$ ruby migrate.rb
** 1 migrations successfully loaded
** Migrating 1-initial-schema
and another time to see that it’s not running the migration twice:
$ ruby migrate.rb
** 1 migrations successfully loaded
Append migrate.rb
to add another migration. It’s very important to not remove the previous migrations as the migrator needs to know about all migrations to work correctly. You should also be aware that the number “2” in the migration name below is not significant, and it’s only the ordering of the #migrate calls that matter.
m.migrate "2-add-bio" do |db|
db.add_column(:users, :bio, :text)
end
We will now see the following:
$ ruby migrate.rb
** 2 migrations successfully loaded
** Migrating 2-add-bio
Moving into files
After working with this system you will find that having the migrations in a single file is quite convenient. You don’t need to worry about generators, and if you forgot the correct way create/remove/alter columns, you can easily peek at the recent migrations.
At a certain point however, the file become very large and hard to work with. Migrator provides a helper method #load_directory to help you split out into multiple files:
# migrate.rb
# (same setup as earlier)
m = Ippon::Migrator.new(db)
m.load_directory(__dir__ + "/migrations")
m.print_summary
m.apply
# migrations/2018-01.rb
migrate "1-initial-schema" do |db|
db.create_table(:users) do
primary_key :id
Text :email, null: false, unique: true
Text :crypted_password
end
end
migrate "2-add-bio" do |db|
db.add_column(:users, :bio, :text)
end
It’s up to you to decide how to structure the files themselves. Migrator will load the files in order based on the filename, and the rest of the structuring is left to you. You might prefer to have one migration per file, one file per month, or split it manully into smaller files when you think it’s too large. Because the migration name is in the code (and not dependent on the filename) you can always restructure later.
Key concepts
You need to know the following concepts:
-
The migrator stores a list of migrations.
-
Every migration has a name (string) and some code which tells how to apply it.
-
You use #migrate to declare migrations. The order in which you call this method decides the order in which migrations are applied.
-
The migration name must be globally unique. It’s recommended to use some sort of manual counter: “2-add-name”. This ensures that if you five months later add a migration named “add-name” it won’t collide with older migrations.
-
Note that the migration counter is not significant in any other way. If two developers create the migrations “2-add-name” and “2-add-bio” in different branches, you should not rename one of them to “3”. You should only make sure that they are declared in a correct order.
-
Use #load_directory to load migrations from different files.
-
You can use #unapplied_names to see if there are migrations that haven’t been applied yet. You can for instance check this right before you boot your web server (or run your test suite) to verify that your database is correctly migrated.
-
The migrator stores information about the currently applied migrations in the
schema_migrations
table. This is created for you. Never touch this table manually.
Defined Under Namespace
Classes: Migration
Instance Method Summary collapse
-
#apply ⇒ Object
Applies the loaded migrations to the database.
-
#initialize(db) ⇒ Migrator
constructor
A new instance of Migrator.
- #load_directory(dir) ⇒ Object
-
#migrate(name) {|db| ... } ⇒ Object
Defines a migration.
-
#print_summary ⇒ Object
Prints a summary of how many migrations were successfully loaded, together with a list of the migrations that have not yet been applied to the database.
-
#unapplied_names ⇒ Array<String>
Returns migrations that have not yet been applied to the database.
Constructor Details
#initialize(db) ⇒ Migrator
Returns a new instance of Migrator.
131 132 133 134 135 |
# File 'lib/ippon/migrator.rb', line 131 def initialize(db) @db = db @seen = Set.new @migrations = [] end |
Instance Method Details
#apply ⇒ Object
Applies the loaded migrations to the database.
167 168 169 170 171 172 |
# File 'lib/ippon/migrator.rb', line 167 def apply @migrations.each do |migration| apply_migration(migration) end nil end |
#load_directory(dir) ⇒ Object
209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/ippon/migrator.rb', line 209 def load_directory(dir) if !File.directory?(dir) raise ArgumentError, "#{dir} is not a directory" end files = Dir[File.join(dir, "*.rb")] files.sort.each do |path| puts "[ ] Loading #{path}" instance_eval(File.read(path), path) end nil end |
#migrate(name) {|db| ... } ⇒ Object
Defines a migration.
154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/ippon/migrator.rb', line 154 def migrate(name, &blk) name = name.to_s if @seen.include?(name) raise ArgumentError, "duplicate migration: #{name}" end @seen << name @migrations << Migration.new(name, blk) nil end |
#print_summary ⇒ Object
Prints a summary of how many migrations were successfully loaded, together with a list of the migrations that have not yet been applied to the database.
201 202 203 204 205 206 |
# File 'lib/ippon/migrator.rb', line 201 def print_summary puts "[ ] #{@migrations.size} migrations successfully loaded" unapplied_names.each do |name| puts "[!] #{name} not applied" end end |
#unapplied_names ⇒ Array<String>
Returns migrations that have not yet been applied to the database.
183 184 185 186 187 188 |
# File 'lib/ippon/migrator.rb', line 183 def unapplied_names seen = dataset.select_map(:name).to_set @migrations .select { |migration| !seen.include?(migration.name) } .map { |migration| migration.name } end |