# frozen_string_literal: true
require "sequel"
require "sequel/extensions/migration"
module Hanami
module Model
# Migration error
#
# @since 0.4.0
class MigrationError < Hanami::Model::Error
end
# Database schema migrator
#
# @since 0.4.0
class Migrator
require "hanami/model/migrator/connection"
require "hanami/model/migrator/adapter"
# 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 :sql, 'postgres://localhost/foo'
# end
#
# Hanami::Model::Migrator.create # Creates `foo' database
#
# NOTE: Class level interface SHOULD be removed in Hanami 2.0
def self.create
new.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 :sql, 'postgres://localhost/foo'
# end
#
# Hanami::Model::Migrator.drop # Drops `foo' database
#
# NOTE: Class level interface SHOULD be removed in Hanami 2.0
def self.drop
new.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
# @see Hanami::Model::Configuration#rollback
#
# @example Migrate Up
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter :sql, '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 :sql, 'postgres://localhost/foo'
# migrations 'db/migrations'
# end
#
# # Reads all files from "db/migrations" and apply them
# Hanami::Model::Migrator.migrate
#
# # Migrate to a specific version
# Hanami::Model::Migrator.migrate(version: "20150610133853")
#
# NOTE: Class level interface SHOULD be removed in Hanami 2.0
def self.migrate(version: nil)
new.migrate(version: version)
end
# Rollback database schema
#
# @param steps [Number,NilClass] number of versions to rollback
#
# @raise [Hanami::Model::MigrationError] if an error occurs
#
# @since 1.1.0
#
# @see Hanami::Model::Configuration#adapter
# @see Hanami::Model::Configuration#migrations
# @see Hanami::Model::Configuration#migrate
#
# @example Rollback
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter :sql, 'postgres://localhost/foo'
# migrations 'db/migrations'
# end
#
# # Reads all files from "db/migrations" and apply them
# Hanami::Model::Migrator.migrate
#
# # By default only rollback one version
# Hanami::Model::Migrator.rollback
#
# # Use a hash passing a number of versions to rollback, it will rollbacks those versions
# Hanami::Model::Migrator.rollback(versions: 2)
#
# NOTE: Class level interface SHOULD be removed in Hanami 2.0
def self.rollback(steps: 1)
new.rollback(steps: steps)
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 :sql, '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
#
# NOTE: Class level interface SHOULD be removed in Hanami 2.0
def self.apply
new.apply
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 :sql, 'postgres://localhost/foo'
# migrations 'db/migrations'
# end
#
# Hanami::Model::Migrator.prepare # => creates `foo' and runs migrations
#
# @example Prepare Database (with schema dump)
# require 'hanami/model'
# require 'hanami/model/migrator'
#
# Hanami::Model.configure do
# # ...
# adapter :sql, '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)
#
# NOTE: Class level interface SHOULD be removed in Hanami 2.0
def self.prepare
new.prepare
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"
#
# NOTE: Class level interface SHOULD be removed in Hanami 2.0
def self.version
new.version
end
# Instantiate a new migrator
#
# @param configuration [Hanami::Model::Configuration] framework configuration
#
# @return [Hanami::Model::Migrator] a new instance
#
# @since 0.7.0
# @api private
def initialize(configuration: self.class.configuration)
@configuration = configuration
@adapter = Adapter.for(configuration)
end
# @since 0.7.0
# @api private
#
# @see Hanami::Model::Migrator.create
def create
adapter.create
end
# @since 0.7.0
# @api private
#
# @see Hanami::Model::Migrator.drop
def drop
adapter.drop
end
# @since 0.7.0
# @api private
#
# @see Hanami::Model::Migrator.migrate
def migrate(version: nil)
adapter.migrate(migrations, version) if migrations?
end
# @since 1.1.0
# @api private
#
# @see Hanami::Model::Migrator.rollback
def rollback(steps: 1)
adapter.rollback(migrations, steps.abs) if migrations?
end
# @since 0.7.0
# @api private
#
# @see Hanami::Model::Migrator.apply
def apply
migrate
adapter.dump
delete_migrations
end
# @since 0.7.0
# @api private
#
# @see Hanami::Model::Migrator.prepare
def prepare
drop
rescue # rubocop:disable Lint/SuppressedException
ensure
create
adapter.load
migrate
end
# @since 0.7.0
# @api private
#
# @see Hanami::Model::Migrator.version
def version
adapter.version
end
# Hanami::Model configuration
#
# @since 0.4.0
# @api private
def self.configuration
Model.configuration
end
private
# @since 0.7.0
# @api private
attr_reader :configuration
# @since 0.7.0
# @api private
attr_reader :connection
# @since 0.7.0
# @api private
attr_reader :adapter
# Migrations directory
#
# @since 0.7.0
# @api private
def migrations
configuration.migrations
end
# Check if there are migrations
#
# @since 0.7.0
# @api private
def migrations?
Dir["#{migrations}/*.rb"].any?
end
# Delete all the migrations
#
# @since 0.7.0
# @api private
def delete_migrations
migrations.each_child(&:delete)
end
end
end
end