require 'sequel'
require 'sequel/extensions/migration'
require 'hanami/model/migrator/connection'
require 'hanami/model/migrator/adapter'
module Hanami
module Model
# Migration error
#
# @since 0.4.0
class MigrationError < Hanami::Model::Error
end
# Define a migration
#
# It must define an up/down strategy to write schema changes (up) and to
# rollback them (down).
#
# We can use up and down blocks for custom strategies, or
# only one change block that automatically implements "down" strategy.
#
# @param blk [Proc] a block that defines up/down or change database migration
#
# @since 0.4.0
#
# @example Use up/down blocks
# Hanami::Model.migration do
# up do
# create_table :books do
# primary_key :id
# column :book, String
# end
# end
#
# down do
# drop_table :books
# end
# end
#
# @example Use change block
# Hanami::Model.migration do
# change do
# create_table :books do
# primary_key :id
# column :book, String
# end
# end
#
# # DOWN strategy is automatically generated
# end
def self.migration(&blk)
Sequel.migration(&blk)
end
# Database schema migrator
#
# @since 0.4.0
module Migrator
# Create database defined by current configuration.
#
# It's only implemented for the following databases:
#
# * SQLite3
# * PostgreSQL
# * MySQL
#
# @raise [Hanami::Model::MigrationError] if an error occurs
#
# @since 0.4.0
#
# @see Hanami::Model::Configuration#adapter
#
# @example
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter type: :sql, uri: 'postgres://localhost/foo'
# end
#
# Hanami::Model::Migrator.create # Creates `foo' database
def self.create
adapter(connection).create
end
# Drop database defined by current configuration.
#
# It's only implemented for the following databases:
#
# * SQLite3
# * PostgreSQL
# * MySQL
#
# @raise [Hanami::Model::MigrationError] if an error occurs
#
# @since 0.4.0
#
# @see Hanami::Model::Configuration#adapter
#
# @example
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter type: :sql, uri: 'postgres://localhost/foo'
# end
#
# Hanami::Model::Migrator.drop # Drops `foo' database
def self.drop
adapter(connection).drop
end
# Migrate database schema
#
# It's possible to migrate "down" by specifying a version
# (eg. "20150610133853")
#
# @param version [String,NilClass] target version
#
# @raise [Hanami::Model::MigrationError] if an error occurs
#
# @since 0.4.0
#
# @see Hanami::Model::Configuration#adapter
# @see Hanami::Model::Configuration#migrations
#
# @example Migrate Up
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter type: :sql, uri: 'postgres://localhost/foo'
# migrations 'db/migrations'
# end
#
# # Reads all files from "db/migrations" and apply them
# Hanami::Model::Migrator.migrate
#
# @example Migrate Down
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter type: :sql, uri: 'postgres://localhost/foo'
# migrations 'db/migrations'
# end
#
# # Reads all files from "db/migrations" and apply them
# Hanami::Model::Migrator.migrate
#
# # Migrate to a specifiy version
# Hanami::Model::Migrator.migrate(version: "20150610133853")
def self.migrate(version: nil)
version = Integer(version) unless version.nil?
Sequel::Migrator.run(connection, migrations, target: version, allow_missing_migration_files: true) if migrations?
rescue Sequel::Migrator::Error => e
raise MigrationError.new(e.message)
end
# Migrate, dump schema, delete migrations.
#
# This is an experimental feature.
# It may change or be removed in the future.
#
# Actively developed applications accumulate tons of migrations.
# In the long term they are hard to maintain and slow to execute.
#
# "Apply" feature solves this problem.
#
# It keeps an updated SQL file with the structure of the database.
# This file can be used to create fresh databases for developer machines
# or during testing. This is faster than to run dozen or hundred migrations.
#
# When we use "apply", it eliminates all the migrations that are no longer
# necessary.
#
# @raise [Hanami::Model::MigrationError] if an error occurs
#
# @since 0.4.0
#
# @see Hanami::Model::Configuration#adapter
# @see Hanami::Model::Configuration#migrations
#
# @example Apply Migrations
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter type: :sql, uri: 'postgres://localhost/foo'
# migrations 'db/migrations'
# schema 'db/schema.sql'
# end
#
# # Reads all files from "db/migrations" and apply and delete them.
# # It generates an updated version of "db/schema.sql"
# Hanami::Model::Migrator.apply
def self.apply
migrate
adapter(connection).dump
delete_migrations
end
# Prepare database: drop, create, load schema (if any), migrate.
#
# This is designed for development machines and testing mode.
# It works faster if used with apply.
#
# @raise [Hanami::Model::MigrationError] if an error occurs
#
# @since 0.4.0
#
# @see Hanami::Model::Migrator.apply
#
# @example Prepare Database
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter type: :sql, uri: 'postgres://localhost/foo'
# migrations 'db/migrations'
# end
#
# Hanami::Model::Migrator.prepare # => creates `foo' and run migrations
#
# @example Prepare Database (with schema dump)
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter type: :sql, uri: 'postgres://localhost/foo'
# migrations 'db/migrations'
# schema 'db/schema.sql'
# end
#
# Hanami::Model::Migrator.apply # => updates schema dump
# Hanami::Model::Migrator.prepare # => creates `foo', load schema and run pending migrations (if any)
def self.prepare
drop rescue nil
create
adapter(connection).load
migrate
end
# Return current database version timestamp
#
# If no migrations were ran, it returns nil.
#
# @return [String,NilClass] current version, if previously migrated
#
# @since 0.4.0
#
# @example
# # Given last migrations is:
# # 20150610133853_create_books.rb
#
# Hanami::Model::Migrator.version # => "20150610133853"
def self.version
adapter(connection).version
end
private
# Loads an adapter for the given connection
#
# @since 0.4.0
# @api private
def self.adapter(connection)
Adapter.for(connection)
end
# Delete all the migrations
#
# @since 0.4.0
# @api private
def self.delete_migrations
migrations.each_child(&:delete)
end
# Database connection
#
# @since 0.4.0
# @api private
def self.connection
Sequel.connect(
configuration.adapter.uri
)
rescue Sequel::AdapterNotFound
raise MigrationError.new("Current adapter (#{ configuration.adapter.type }) doesn't support SQL database operations.")
end
# Hanami::Model configuration
#
# @since 0.4.0
# @api private
def self.configuration
Model.configuration
end
# Migrations directory
#
# @since 0.4.0
# @api private
def self.migrations
configuration.migrations
end
# Check if there are migrations
#
# @since 0.4.0
# @api private
def self.migrations?
Dir["#{ migrations }/*.rb"].any?
end
end
end
end