Awesome
[WIP] ROM::Migrator
Base class for ROM migrators.
Installation
Add this line to your application's Gemfile:
# Gemfile
gem "rom-migrator"
Then execute:
bundle
Or add it manually:
gem install rom-migrator
Usage
When creating custom ROM adapter, you should implement its own migrator inherited from ROM::Migrator
:
and customize it as following:
- provide adapter-specific methods to register applied migrations
- provide adapter-specific methods to make changes to persistence via ROM gateway
You can also redefine some default settings, namely:
Load the Gem
Install and require the gem. It is not loaded by default because there is a bunch of adapters that doesn't need mingrators.
# lib/rom-custom_adapter.rb
require "rom"
require "rom-migrator"
Provide the Migrator
Subclass the migrator from ROM::Migrator
:
# lib/rom-custom_adapter/migrator.rb
module ROM::CustomAdapter
class Migrator < ROM::Migrator
end
end
Define how to register migrations
You MUST define 4 adapter-specific operations, that allow migrator to register/unregister applied migration, and find their numbers.
Blocks are called in the context of the corresponding gateway:
# Suppose the gateway responds to #send_query
# lib/rom-custom_adapter/migrator.rb
module ROM::CustomAdapter
class Migrator < ROM::Migrator
# ...
prepare_registry do
send_query "CREATE TABLE IF NOT EXISTS rom_custom_adapter_migrations;"
end
register do |number|
send_query "INSERT number = '#{number}' INTO rom_custom_adapter_migrations;"
end
unregister do |number|
send_query "DELETE FROM rom_custom_adapter_migrations WHERE number = '#{number}';"
end
registered do
send_query("SELECT number FROM rom_custom_adapter_migrations;").map(&:number)
end
end
end
You aren't restricted by a gateway as a storage of migrations. The same API can be implemented using a file system or remote server.
For example you can create / remove a file with a corresponding number inside a special folder:
module ROM::CustomAdapter
class Migrator < ROM::Migrator
REGISTRY = "db/migrate/applied_migrations"
prepare_registry { FileUtils.mkdir_p REGISTRY }
register { |number| FileUtils.touch "#{REGISTRY}/.#{number}" }
unregister { |number| FileUtils.rm_f "#{REGISTRY}/.#{number}" }
registered { Dir["#{REGISTRY}/.*"].map { |fn| fn[/\.[^.]$/] } }
end
end
Customize default path to migrations
By default migrations are expected to be found in db/migrate
. You can redefine this settings for custom adapter:
# lib/rom-custom_adapter/migrator.rb
module ROM::CustomAdapter
class Migrator < ROM::Migrator
# ...
default_path "db/migrate/custom_adapter"
# ...
end
end
Customize migration number counter
Reload #default_counter
method, that defines the number of the migration being generated.
By default the number is a timestamp in %Y%m%d%H%M%S%L
format (17 digits for the current UTC time in milliseconds).
Notice, that migrator will order migrations by stringified numbers in the ascending order. That's why stringified output of the method MUST be greater than its stringified argument. See inline comments below for the contract:
# lib/rom-custom_adapter/migrator.rb
module ROM::CustomAdapter
class Migrator < ROM::Migrator
# ...
default_counter { |last_number| last_number.to_i + 1 }
# ...
end
end
Customize migration template
The default template is provided by the rom-migrator
gem.
You can customize the template, for example, to add comments with available methods as shown below.
# lib/rom-custom_adapter/migration.txt
ROM::Migrator.migration do
up do
# create_table(:table_name).set(name: :text, age: :int).primary_key(:name)
end
down do
# drop_table(:table_name)
end
end
Let the migrator to know a path to the template:
# lib/rom-custom_adapter/migrator.rb
module ROM::CustomAdapter
class Migrator < ROM::Migrator
# ...
default_template File.expand_path("../migration.txt", __FILE__)
# ...
end
end
Using Migrator in Application
To use a migrator you have to set rom environment and prepare a +gateway+:
require "rom-custom_adapter"
# Set rom environment
env = ROM::Environment.new
env.use :auto_registration
setup = env.setup :custom_adapter #, whatever additional settings
# ...
setup.finalize
rom = setup.env
# Use a gateway
gateway = rom.gateways[:default]
Then access the migrator via corresponding Gateway:
migrator = gateway.migrator
By default the migrator will look for migrations in the adapter-specific default path. You can change the path explicitly:
migrator = gateway.migrator path: "db/migrate/custom_adapter"
You can also refer to a list of folders, containing migrations:
migrator = gateway.migrator paths: ["db/migrate", "spec/dummy/db/migrate"]
The migrator publishes log messages to $stdout
. To change this option you can set a custom logger (some kind of ruby ::Logger
klass):
logger = ::Logger.new(::StringIO.new)
migrator = gateway.migrator logger: logger
You can customize a template for migrations and a counter as well:
migrator = gateway.migrator template: "config/migration.txt", counter: proc { |_| Time.now.strftime "%Y%m%d%H%M%#{rand(1000..9999)}" }
Building inline migration
You can build and apply the migration inline:
migration = migrator.migration do
up do
create_table :foo
end
down do
drop_table :foo
end
end
migration.apply # changes the persistence
migration.reverse # reverses the changes
By default migration number will be provided via counter. You can set the number explicitly:
migrator.migration(number: "1") do
# ...
end
Running migrations
Use the apply
and reverse
methods to apply or reverse migrations:
migrator.apply # runs all migrations from the default folder
migrator.reverse # reverses migrations back
Both methods take an option :target
for a version to migrate persistence to.
# All migrations, that hasn't been applied before, will be applied
migrator.apply
# Only those migrations, that hasn't been applied before,
# and whose numbers not greater when the target, will be applied
migrator.apply target: "20170101234319"
# All registered (applied) migrations, whose numbers not less when the target,
# will be reversed
migrator.reverse target: "20170101234319"
When reversing a migration you can also use :allow_missing_files
option. In this case when migrator will try to reverse a number that is absent on disk, it will unregister the number and keep reversion further.
migrator.reverse allow_missing_files: true
Otherwise it will raise an exception complaining it has no recipy how to roll back the migrations.
Scaffolding a Migration
Use the #generator
method to scaffold new migration. You MUST provide the name of the migration class:
migrator.create_file
# => `db/migrate/{next_number}.rb
You can customize options: :path
, :number
and :name
for migration, :logger
and :template
.
migrator.create path: "db/migrate/custom", name: "create_users", number: "1"
# => `db/migrate/custom/1_create_users.rb
Compatibility
Tested under rubies compatible to MRI 1.9+.
Uses RSpec 3.0+ for testing and hexx-suit for dev/test tools collection.
Contributing
- Fork the project
- Create your feature branch (
git checkout -b my-new-feature
) - Add tests for it
- Run
rubocop
andinch --pedantic
to ensure the style and inline docs are ok - Run
rake mutant
orrake exhort
to ensure 100% mutant-proof coverage - Commit your changes (
git commit -am '[UPDATE] Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
License
See the MIT LICENSE.