rom-rb/rom-migrator

View on GitHub
README.md

Summary

Maintainability
Test Coverage
[WIP] ROM::Migrator
===================

[![Gem Version](https://img.shields.io/gem/v/rom-migrator.svg?style=flat)][gem]
[![Build Status](https://img.shields.io/travis/rom-rb/rom-migrator/master.svg?style=flat)][travis]
[![Dependency Status](https://img.shields.io/gemnasium/rom-rb/rom-migrator.svg?style=flat)][gemnasium]
[![Code Climate](https://img.shields.io/codeclimate/github/rom-rb/rom-migrator.svg?style=flat)][codeclimate]
[![Coverage](https://img.shields.io/coveralls/rom-rb/rom-migrator.svg?style=flat)][coveralls]
[![Inline docs](http://inch-ci.org/github/rom-rb/rom-migrator.svg)][inch]

Base class for [ROM][rom] migrators.

Installation
------------

Add this line to your application's Gemfile:

```ruby
# 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`:

- [load the gem](#load-the-gem)
- [provide the migrator](#provide-the-migrator)

and customize it as following:

- provide *adapter-specific* methods to [register applied migrations](#define-how-to-register-migrations)
- provide *adapter-specific* methods to [make changes to persistence](#implement-methods-to-make-changes-to-persistence) via ROM gateway

You can also redefine some default settings, namely:

- [default path to migrations](#customize-default-path-to-migrations) (`db/migrate`)
- [path to template for migrations](#customize-migration-template)
- [migration number counter](#customize-migration-number-counter)

### 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.

```ruby
# lib/rom-custom_adapter.rb
require "rom"
require "rom-migrator"
```

### Provide the Migrator

Subclass the migrator from `ROM::Migrator`:

```ruby
# 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:

```ruby
# 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:

```ruby
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:

```ruby
# 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:

```ruby
# 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:

```ruby
# 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+:

```ruby
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:

```ruby
migrator = gateway.migrator
```

By default the migrator will look for migrations in the [adapter-specific default path](#customize-default-path-to-migrations). You can change the path explicitly:

```ruby
migrator = gateway.migrator path: "db/migrate/custom_adapter"
```

You can also refer to a list of folders, containing migrations:

```ruby
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):

```ruby
logger = ::Logger.new(::StringIO.new)
migrator = gateway.migrator logger: logger
```

You can customize a template for migrations and a counter as well:

```ruby
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:

```ruby
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:

```ruby
migrator.migration(number: "1") do
  # ...
end
```

### Running migrations

Use the `apply` and `reverse` methods to apply or reverse migrations:

```ruby
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.

```ruby
# 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.

```ruby
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:

```ruby
migrator.create_file
# => `db/migrate/{next_number}.rb
```

You can customize options: `:path`, `:number` and `:name` for migration, `:logger` and `:template`.

```ruby
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+][rubies].

Uses [RSpec][rspec] 3.0+ for testing and [hexx-suit][hexx-suit] for dev/test tools collection.

Contributing
------------

* [Fork the project][github]
* Create your feature branch (`git checkout -b my-new-feature`)
* Add tests for it
* Run `rubocop` and `inch --pedantic` to ensure the style and inline docs are ok
* Run `rake mutant` or `rake exhort` to ensure 100% [mutant-proof][mutant] 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](LICENSE).

[codeclimate]: https://codeclimate.com/github/rom-rb/rom-migrator
[coveralls]: https://coveralls.io/r/rom-rb/rom-migrator
[gem]: https://rubygems.org/gems/rom-migrator
[gemnasium]: https://gemnasium.com/rom-rb/rom-migrator
[github]: https://github.com/rom-rb/rom
[guide]: http://rom-rb.org/guides/adapters/how-to/
[hexx-suit]: https://github.com/nepalez/hexx-suit
[inch]: https://inch-ci.org/github/rom-rb/rom-migrator
[license]: LICENSE
[mutant]: https://github.com/mbj/mutant
[rom]: http://rom-rb.org
[rspec]: http://rspec.org
[rubies]: .travis.yml
[travis]: https://travis-ci.org/rom-rb/rom-migrator