Home

Awesome

[WIP] ROM::Migrator

Gem Version Build Status Dependency Status Code Climate Coverage Inline docs

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:

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

License

See the MIT LICENSE.