# 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