module ActiveRecord
class IrreversibleMigration < ActiveRecordError#:nodoc:
end
class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
# Migrations can manage the evolution of a schema used by several physical databases. It's a solution
# to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
# push that change to other developers and to the production server. With migrations, you can describe the transformations
# in self-contained classes that can be checked into version control systems and executed against another database that
# might be one, two, or five versions behind.
#
# Example of a simple migration:
#
# class AddSsl < ActiveRecord::Migration
# def self.up
# add_column :accounts, :ssl_enabled, :boolean, :default => 1
# end
#
# def self.down
# remove_column :accounts, :ssl_enabled
# end
# end
#
# This migration will add a boolean flag to the accounts table and remove it again, if you're backing out of the migration.
# It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
# or remove the migration. These methods can consist of both the migration specific methods, like add_column and remove_column,
# but may also contain regular Ruby code for generating data needed for the transformations.
#
# Example of a more complex migration that also needs to initialize data:
#
# class AddSystemSettings < ActiveRecord::Migration
# def self.up
# create_table :system_settings do |t|
# t.column :name, :string
# t.column :label, :string
# t.column :value, :text
# t.column :type, :string
# t.column :position, :integer
# end
#
# SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
# end
#
# def self.down
# drop_table :system_settings
# end
# end
#
# This migration first adds the system_settings table, then creates the very first row in it using the Active Record model
# that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema
# in one block call.
#
# == Available transformations
#
# * create_table(name, options) Creates a table called +name+ and makes the table object available to a block
# that can then add columns to it, following the same format as add_column. See example above. The options hash is for
# fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition.
# * drop_table(name): Drops the table called +name+.
# * add_column(table_name, column_name, type, options): Adds a new column to the table called +table_name+
# named +column_name+ specified to be one of the following types:
# :string, :text, :integer, :float, :datetime, :timestamp, :time, :date, :binary, :boolean. A default value can be specified
# by passing an +options+ hash like { :default => 11 }.
# * rename_column(table_name, column_name, new_column_name): Renames a column but keeps the type and content.
# * change_column(table_name, column_name, type, options): Changes the column to a different type using the same
# parameters as add_column.
# * remove_column(table_name, column_name): Removes the column named +column_name+ from the table called +table_name+.
# * add_index(table_name, column_name, index_type): Add a new index with the name of the column on the column. Specify an optional index_type (e.g. UNIQUE).
# * remove_index(table_name, column_name): Remove the index called the same as the column.
#
# == Irreversible transformations
#
# Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
# an IrreversibleMigration exception in their +down+ method.
#
# == Running migrations from within Rails
#
# The Rails package has support for migrations with the script/generate migration my_new_migration command and
# with the rake migrate command that'll run all the pending migrations. It'll even create the needed schema_info
# table automatically if it's missing.
#
# == Database support
#
# Migrations are currently only supported in MySQL and PostgreSQL.
#
# == More examples
#
# Not all migrations change the schema. Some just fix the data:
#
# class RemoveEmptyTags < ActiveRecord::Migration
# def self.up
# Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
# end
#
# def self.down
# # not much we can do to restore deleted data
# raise IrreversibleMigration
# end
# end
#
# Others remove columns when they migrate up instead of down:
#
# class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
# def self.up
# remove_column :items, :incomplete_items_count
# remove_column :items, :completed_items_count
# end
#
# def self.down
# add_column :items, :incomplete_items_count
# add_column :items, :completed_items_count
# end
# end
#
# And sometimes you need to do something in SQL not abstracted directly by migrations:
#
# class MakeJoinUnique < ActiveRecord::Migration
# def self.up
# execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
# end
#
# def self.down
# execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
# end
# end
#
# == Using the class after changing table
#
# Sometimes you'll want to add a column in a migration and populate it immediately after. In that case, you'll need
# to make a call to Base#reset_column_information in order to ensure that the class has the latest column data from
# after the new column was added. Example:
#
# class MakeJoinUnique < ActiveRecord::Migration
# def self.up
# add_column :people, :salary, :integer
# Person.find(:all).each do |p|
# p.salary = SalaryCalculator.compute(p)
# end
# end
# end
class Migration
class << self
def up() end
def down() end
private
def method_missing(method, *arguments, &block)
arguments[0] = Migrator.proper_table_name(arguments.first) unless arguments.empty?
ActiveRecord::Base.connection.send(method, *arguments, &block)
end
end
end
class Migrator#:nodoc:
class << self
def migrate(migrations_path, target_version = nil)
Base.connection.initialize_schema_information
case
when target_version.nil?, current_version < target_version
up(migrations_path, target_version)
when current_version > target_version
down(migrations_path, target_version)
when current_version == target_version
return # You're on the right version
end
end
def up(migrations_path, target_version = nil)
self.new(:up, migrations_path, target_version).migrate
end
def down(migrations_path, target_version = nil)
self.new(:down, migrations_path, target_version).migrate
end
def schema_info_table_name
Base.table_name_prefix + "schema_info" + Base.table_name_suffix
end
def current_version
(Base.connection.select_one("SELECT version FROM #{schema_info_table_name}") || {"version" => 0})["version"].to_i
end
def proper_table_name(name)
# Use the ActiveRecord objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
end
end
def initialize(direction, migrations_path, target_version = nil)
raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
@direction, @migrations_path, @target_version = direction, migrations_path, target_version
Base.connection.initialize_schema_information
end
def current_version
self.class.current_version
end
def migrate
migration_classes.each do |(version, migration_class)|
Base.logger.info("Reached target version: #{@target_version}") and break if reached_target_version?(version)
next if irrelevant_migration?(version)
Base.logger.info "Migrating to #{migration_class} (#{version})"
migration_class.send(@direction)
set_schema_version(version)
end
end
private
def migration_classes
migrations = migration_files.inject([]) do |migrations, migration_file|
load(migration_file)
version, name = migration_version_and_name(migration_file)
assert_unique_migration_version(migrations, version.to_i)
migrations << [ version.to_i, migration_class(name) ]
end
down? ? migrations.sort.reverse : migrations.sort
end
def assert_unique_migration_version(migrations, version)
if !migrations.empty? && migrations.transpose.first.include?(version)
raise DuplicateMigrationVersionError.new(version)
end
end
def migration_files
files = Dir["#{@migrations_path}/[0-9]*_*.rb"].sort_by do |f|
migration_version_and_name(f).first.to_i
end
down? ? files.reverse : files
end
def migration_class(migration_name)
migration_name.camelize.constantize
end
def migration_version_and_name(migration_file)
return *migration_file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
end
def set_schema_version(version)
Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{down? ? version.to_i - 1 : version.to_i}")
end
def up?
@direction == :up
end
def down?
@direction == :down
end
def reached_target_version?(version)
(up? && version.to_i - 1 == @target_version) || (down? && version.to_i == @target_version)
end
def irrelevant_migration?(version)
(up? && version.to_i <= current_version) || (down? && version.to_i > current_version)
end
end
end