module ActiveRecord class IrreversibleMigration < ActiveRecordError#:nodoc: end class DuplicateMigrationVersionError < ActiveRecordError#:nodoc: def initialize(version) super("Multiple migrations have the version number #{version}") end end class UnknownMigrationVersionError < ActiveRecordError #:nodoc: def initialize(version) super("No migration with version number #{version}") end end class IllegalMigrationNameError < ActiveRecordError#:nodoc: def initialize(name) super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)") 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 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.string :name # t.string :label # t.text :value # t.string :type # t.integer :position # 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+. # * rename_table(old_name, new_name): Renames the table called +old_name+ to +new_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, :decimal, :datetime, :timestamp, :time, # :date, :binary, :boolean. A default value can be specified by passing an # +options+ hash like { :default => 11 }. Other options include :limit and :null (e.g. { :limit => 50, :null => false }) # -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details. # * 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_names, options): Adds a new index with the name of the column. Other options include # :name and :unique (e.g. { :name => "users_name_index", :unique => true }). # * remove_index(table_name, index_name): Removes the index specified by +index_name+. # # == Irreversible transformations # # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise # an ActiveRecord::IrreversibleMigration exception in their +down+ method. # # == Running migrations from within Rails # # The Rails package has several tools to help create and apply migrations. # # To generate a new migration, you can use # script/generate migration MyNewMigration # # where MyNewMigration is the name of your migration. The generator will # create an empty migration file nnn_my_new_migration.rb in the db/migrate/ # directory where nnn is the next largest migration number. # # You may then edit the self.up and self.down methods of # MyNewMigration. # # There is a special syntactic shortcut to generate migrations that add fields to a table. # script/generate migration add_fieldname_to_tablename fieldname:string # # This will generate the file nnn_add_fieldname_to_tablename, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration # def self.up # add_column :tablenames, :fieldname, :string # end # # def self.down # remove_column :tablenames, :fieldname # end # end # # To run migrations against the currently configured database, use # rake db:migrate. This will update the database by running all of the # pending migrations, creating the schema_info table if missing. # # To roll the database back to a previous migration version, use # rake db:migrate VERSION=X where X is the version to which # you wish to downgrade. If any of the migrations throw an # ActiveRecord::IrreversibleMigration exception, that step will fail and you'll # have some manual work to do. # # == Database support # # Migrations are currently supported in MySQL, PostgreSQL, SQLite, # SQL Server, Sybase, and Oracle (all supported databases except DB2). # # == 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 ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags" # 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 a model after changing its 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 model has the latest column data from # after the new column was added. Example: # # class AddPeopleSalary < ActiveRecord::Migration # def self.up # add_column :people, :salary, :integer # Person.reset_column_information # Person.find(:all).each do |p| # p.update_attribute :salary, SalaryCalculator.compute(p) # end # end # end # # == Controlling verbosity # # By default, migrations will describe the actions they are taking, writing # them to the console as they happen, along with benchmarks describing how # long each step took. # # You can quiet them down by setting ActiveRecord::Migration.verbose = false. # # You can also insert your own messages and benchmarks by using the #say_with_time # method: # # def self.up # ... # say_with_time "Updating salaries..." do # Person.find(:all).each do |p| # p.update_attribute :salary, SalaryCalculator.compute(p) # end # end # ... # end # # The phrase "Updating salaries..." would then be printed, along with the # benchmark for the block when the block completes. class Migration @@verbose = true cattr_accessor :verbose class << self def up_with_benchmarks #:nodoc: migrate(:up) end def down_with_benchmarks #:nodoc: migrate(:down) end # Execute this migration in the named direction def migrate(direction) return unless respond_to?(direction) case direction when :up then announce "migrating" when :down then announce "reverting" end result = nil time = Benchmark.measure { result = send("#{direction}_without_benchmarks") } case direction when :up then announce "migrated (%.4fs)" % time.real; write when :down then announce "reverted (%.4fs)" % time.real; write end result end # Because the method added may do an alias_method, it can be invoked # recursively. We use @ignore_new_methods as a guard to indicate whether # it is safe for the call to proceed. def singleton_method_added(sym) #:nodoc: return if defined?(@ignore_new_methods) && @ignore_new_methods begin @ignore_new_methods = true case sym when :up, :down klass = (class << self; self; end) klass.send(:alias_method_chain, sym, "benchmarks") end ensure @ignore_new_methods = false end end def write(text="") puts(text) if verbose end def announce(message) text = "#{@version} #{name}: #{message}" length = [0, 75 - text.length].max write "== %s %s" % [text, "=" * length] end def say(message, subitem=false) write "#{subitem ? " ->" : "--"} #{message}" end def say_with_time(message) say(message) result = nil time = Benchmark.measure { result = yield } say "%.4fs" % time.real, :subitem say("#{result} rows", :subitem) if result.is_a?(Integer) result end def suppress_messages save, self.verbose = verbose, false yield ensure self.verbose = save end def method_missing(method, *arguments, &block) arg_list = arguments.map(&:inspect) * ', ' say_with_time "#{method}(#{arg_list})" do unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) end ActiveRecord::Base.connection.send(method, *arguments, &block) end end end end class Migrator#:nodoc: class << self def migrate(migrations_path, target_version = nil) 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 rollback(migrations_path, steps=1) migrator = self.new(:down, migrations_path) start_index = migrator.migrations.index(migrator.current_migration) return unless start_index finish = migrator.migrations[start_index + steps] down(migrations_path, finish ? finish.version : 0) 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 run(direction, migrations_path, target_version) self.new(direction, migrations_path, target_version) end def schema_info_table_name Base.table_name_prefix + "schema_info" + Base.table_name_suffix end def current_version Base.connection.select_value("SELECT version FROM #{schema_info_table_name}").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? Base.connection.initialize_schema_information @direction, @migrations_path, @target_version = direction, migrations_path, target_version end def current_version self.class.current_version end def current_migration migrations.detect { |m| m.version == current_version } end def run target = migrations.detect { |m| m.version == @target_version } raise UnknownMigrationVersionError.new(@target_version) if target.nil? target.migrate(@direction) end def migrate current = migrations.detect { |m| m.version == current_version } target = migrations.detect { |m| m.version == @target_version } if target.nil? && !@target_version.nil? && @target_version > 0 raise UnknownMigrationVersionError.new(@target_version) end start = migrations.index(current) || 0 finish = migrations.index(target) || migrations.size - 1 runnable = migrations[start..finish] # skip the current migration if we're heading upwards runnable.shift if up? && runnable.first == current # skip the last migration if we're headed down, but not ALL the way down runnable.pop if down? && !target.nil? runnable.each do |migration| Base.logger.info "Migrating to #{migration} (#{migration.version})" migration.migrate(@direction) set_schema_version_after_migrating(migration) end end def migrations @migrations ||= begin files = Dir["#{@migrations_path}/[0-9]*_*.rb"] migrations = files.inject([]) do |klasses, file| version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first raise IllegalMigrationNameError.new(f) unless version version = version.to_i if klasses.detect { |m| m.version == version } raise DuplicateMigrationVersionError.new(version) end load(file) klasses << returning(name.camelize.constantize) do |klass| class << klass; attr_accessor :version end klass.version = version end end migrations = migrations.sort_by(&:version) down? ? migrations.reverse : migrations end end def pending_migrations migrations.select { |m| m.version > current_version } end private def set_schema_version_after_migrating(migration) version = migration.version if down? after = migrations[migrations.index(migration) + 1] version = after ? after.version : 0 end Base.connection.update("UPDATE #{self.class.schema_info_table_name} SET version = #{version}") end def up? @direction == :up end def down? @direction == :down end end end