diff options
Diffstat (limited to 'activerecord/lib/active_record/migration.rb')
-rw-r--r-- | activerecord/lib/active_record/migration.rb | 1386 |
1 files changed, 1386 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb new file mode 100644 index 0000000000..eca64eb380 --- /dev/null +++ b/activerecord/lib/active_record/migration.rb @@ -0,0 +1,1386 @@ +# frozen_string_literal: true + +require "benchmark" +require "set" +require "zlib" +require "active_support/core_ext/module/attribute_accessors" + +module ActiveRecord + class MigrationError < ActiveRecordError#:nodoc: + def initialize(message = nil) + message = "\n\n#{message}\n\n" if message + super + end + end + + # Exception that can be raised to stop migrations from being rolled back. + # For example the following migration is not reversible. + # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error. + # + # class IrreversibleMigrationExample < ActiveRecord::Migration[5.0] + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<~SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # end + # + # There are two ways to mitigate this problem. + # + # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>: + # + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] + # def up + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<~SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # def down + # execute <<~SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # + # drop_table :distributors + # end + # end + # + # 2. Use the #reversible method in <tt>#change</tt> method: + # + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # reversible do |dir| + # dir.up do + # execute <<~SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # dir.down do + # execute <<~SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # end + # end + # end + # end + class IrreversibleMigration < MigrationError + end + + class DuplicateMigrationVersionError < MigrationError#:nodoc: + def initialize(version = nil) + if version + super("Multiple migrations have the version number #{version}.") + else + super("Duplicate migration version error.") + end + end + end + + class DuplicateMigrationNameError < MigrationError#:nodoc: + def initialize(name = nil) + if name + super("Multiple migrations have the name #{name}.") + else + super("Duplicate migration name.") + end + end + end + + class UnknownMigrationVersionError < MigrationError #:nodoc: + def initialize(version = nil) + if version + super("No migration with version number #{version}.") + else + super("Unknown migration version.") + end + end + end + + class IllegalMigrationNameError < MigrationError#:nodoc: + def initialize(name = nil) + if name + super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).") + else + super("Illegal name for migration.") + end + end + end + + class PendingMigrationError < MigrationError#:nodoc: + def initialize(message = nil) + if !message && defined?(Rails.env) + super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate RAILS_ENV=#{::Rails.env}") + elsif !message + super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate") + else + super + end + end + end + + class ConcurrentMigrationError < MigrationError #:nodoc: + DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running." + RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock" + + def initialize(message = DEFAULT_MESSAGE) + super + end + end + + class NoEnvironmentInSchemaError < MigrationError #:nodoc: + def initialize + msg = "Environment data not found in the schema. To resolve this issue, run: \n\n rails db:environment:set" + if defined?(Rails.env) + super("#{msg} RAILS_ENV=#{::Rails.env}") + else + super(msg) + end + end + end + + class ProtectedEnvironmentError < ActiveRecordError #:nodoc: + def initialize(env = "production") + msg = +"You are attempting to run a destructive action against your '#{env}' database.\n" + msg << "If you are sure you want to continue, run the same command with the environment variable:\n" + msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" + super(msg) + end + end + + class EnvironmentMismatchError < ActiveRecordError + def initialize(current: nil, stored: nil) + msg = +"You are attempting to modify a database that was last run in `#{ stored }` environment.\n" + msg << "You are running in `#{ current }` environment. " + msg << "If you are sure you want to continue, first set the environment using:\n\n" + msg << " rails db:environment:set" + if defined?(Rails.env) + super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") + else + super("#{msg}\n\n") + end + end + end + + # = Active Record Migrations + # + # 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[5.0] + # def up + # add_column :accounts, :ssl_enabled, :boolean, default: true + # end + # + # def 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 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[5.0] + # def 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 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 + # + # === Creation + # + # * <tt>create_join_table(table_1, table_2, options)</tt>: Creates a join + # table having its name as the lexical order of the first two + # arguments. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table for + # details. + # * <tt>create_table(name, options)</tt>: 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. + # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column + # to the table called +table_name+ + # named +column_name+ specified to be one of the following types: + # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, + # <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, + # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be + # specified by passing an +options+ hash like <tt>{ default: 11 }</tt>. + # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. + # <tt>{ limit: 50, null: false }</tt>) -- see + # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. + # * <tt>add_foreign_key(from_table, to_table, options)</tt>: Adds a new + # foreign key. +from_table+ is the table with the key column, +to_table+ contains + # the referenced primary key. + # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index + # with the name of the column. Other options include + # <tt>:name</tt>, <tt>:unique</tt> (e.g. + # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> + # (e.g. <tt>{ order: { name: :desc } }</tt>). + # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column + # +reference_name_id+ by default an integer. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. + # * <tt>add_timestamps(table_name, options)</tt>: Adds timestamps (+created_at+ + # and +updated_at+) columns to +table_name+. + # + # === Modification + # + # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes + # the column to a different type using the same parameters as add_column. + # * <tt>change_column_default(table_name, column_name, default_or_changes)</tt>: + # Sets a default value for +column_name+ defined by +default_or_changes+ on + # +table_name+. Passing a hash containing <tt>:from</tt> and <tt>:to</tt> + # as +default_or_changes+ will make this change reversible in the migration. + # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>: + # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag + # indicates whether the value can be +NULL+. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#change_column_null for + # details. + # * <tt>change_table(name, options)</tt>: Allows to make column alterations to + # the table called +name+. It makes the table object available to a block that + # can then add/remove columns, indexes or foreign keys to it. + # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames + # a column but keeps the type and content. + # * <tt>rename_index(table_name, old_name, new_name)</tt>: Renames an index. + # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ + # to +new_name+. + # + # === Deletion + # + # * <tt>drop_table(name)</tt>: Drops the table called +name+. + # * <tt>drop_join_table(table_1, table_2, options)</tt>: Drops the join table + # specified by the given arguments. + # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column + # named +column_name+ from the table called +table_name+. + # * <tt>remove_columns(table_name, *column_names)</tt>: Removes the given + # columns from the table definition. + # * <tt>remove_foreign_key(from_table, options_or_to_table)</tt>: Removes the + # given foreign key from the table called +table_name+. + # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index + # specified by +column_names+. + # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index + # specified by +index_name+. + # * <tt>remove_reference(table_name, ref_name, options)</tt>: Removes the + # reference(s) on +table_name+ specified by +ref_name+. + # * <tt>remove_timestamps(table_name, options)</tt>: Removes the timestamp + # columns (+created_at+ and +updated_at+) from the table definition. + # + # == Irreversible transformations + # + # Some transformations are destructive in a manner that cannot be reversed. + # Migrations of that kind should raise an <tt>ActiveRecord::IrreversibleMigration</tt> + # 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 + # rails generate migration MyNewMigration + # + # where MyNewMigration is the name of your migration. The generator will + # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> + # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the + # UTC formatted date and time that the migration was generated. + # + # There is a special syntactic shortcut to generate migrations that add fields to a table. + # + # rails generate migration add_fieldname_to_tablename fieldname:string + # + # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this: + # class AddFieldnameToTablename < ActiveRecord::Migration[5.0] + # def change + # add_column :tablenames, :fieldname, :string + # end + # end + # + # To run migrations against the currently configured database, use + # <tt>rails db:migrate</tt>. This will update the database by running all of the + # pending migrations, creating the <tt>schema_migrations</tt> table + # (see "About the schema_migrations table" section below) if missing. It will also + # invoke the db:schema:dump command, which will update your db/schema.rb file + # to match the structure of your database. + # + # To roll the database back to a previous migration version, use + # <tt>rails db:rollback VERSION=X</tt> where <tt>X</tt> is the version to which + # you wish to downgrade. Alternatively, you can also use the STEP option if you + # wish to rollback last few migrations. <tt>rails db:rollback STEP=2</tt> will rollback + # the latest two migrations. + # + # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> 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, and Oracle (all supported databases except DB2). + # + # == More examples + # + # Not all migrations change the schema. Some just fix the data: + # + # class RemoveEmptyTags < ActiveRecord::Migration[5.0] + # def up + # Tag.all.each { |tag| tag.destroy if tag.pages.empty? } + # end + # + # def 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[5.0] + # def up + # remove_column :items, :incomplete_items_count + # remove_column :items, :completed_items_count + # end + # + # def 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[5.0] + # def up + # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" + # end + # + # def 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 + # <tt>Base#reset_column_information</tt> in order to ensure that the model has the + # latest column data from after the new column was added. Example: + # + # class AddPeopleSalary < ActiveRecord::Migration[5.0] + # def up + # add_column :people, :salary, :integer + # Person.reset_column_information + # Person.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 up + # ... + # say_with_time "Updating salaries..." do + # Person.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. + # + # == Timestamped Migrations + # + # By default, Rails generates migrations that look like: + # + # 20080717013526_your_migration_name.rb + # + # The prefix is a generation timestamp (in UTC). + # + # If you'd prefer to use numeric prefixes, you can turn timestamped migrations + # off by setting: + # + # config.active_record.timestamped_migrations = false + # + # In application.rb. + # + # == Reversible Migrations + # + # Reversible migrations are migrations that know how to go +down+ for you. + # You simply supply the +up+ logic, and the Migration system figures out + # how to execute the down commands for you. + # + # To define a reversible migration, define the +change+ method in your + # migration like this: + # + # class TenderloveMigration < ActiveRecord::Migration[5.0] + # def change + # create_table(:horses) do |t| + # t.column :content, :text + # t.column :remind_at, :datetime + # end + # end + # end + # + # This migration will create the horses table for you on the way up, and + # automatically figure out how to drop the table on the way down. + # + # Some commands like +remove_column+ cannot be reversed. If you care to + # define how to move up and down in these cases, you should define the +up+ + # and +down+ methods as before. + # + # If a command cannot be reversed, an + # <tt>ActiveRecord::IrreversibleMigration</tt> exception will be raised when + # the migration is moving down. + # + # For a list of commands that are reversible, please see + # <tt>ActiveRecord::Migration::CommandRecorder</tt>. + # + # == Transactional Migrations + # + # If the database adapter supports DDL transactions, all migrations will + # automatically be wrapped in a transaction. There are queries that you + # can't execute inside a transaction though, and for these situations + # you can turn the automatic transactions off. + # + # class ChangeEnum < ActiveRecord::Migration[5.0] + # disable_ddl_transaction! + # + # def up + # execute "ALTER TYPE model_size ADD VALUE 'new_value'" + # end + # end + # + # Remember that you can still open your own transactions, even if you + # are in a Migration with <tt>self.disable_ddl_transaction!</tt>. + class Migration + autoload :CommandRecorder, "active_record/migration/command_recorder" + autoload :Compatibility, "active_record/migration/compatibility" + + # This must be defined before the inherited hook, below + class Current < Migration # :nodoc: + end + + def self.inherited(subclass) # :nodoc: + super + if subclass.superclass == Migration + raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \ + "Please specify the Rails release the migration was written for:\n" \ + "\n" \ + " class #{subclass} < ActiveRecord::Migration[4.2]" + end + end + + def self.[](version) + Compatibility.find(version) + end + + def self.current_version + ActiveRecord::VERSION::STRING.to_f + end + + MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc: + + # This class is used to verify that all migrations have been run before + # loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load + class CheckPending + def initialize(app) + @app = app + @last_check = 0 + end + + def call(env) + mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime + end + @app.call(env) + end + + private + + def connection + ActiveRecord::Base.connection + end + end + + class << self + attr_accessor :delegate # :nodoc: + attr_accessor :disable_ddl_transaction # :nodoc: + + def nearest_delegate # :nodoc: + delegate || superclass.nearest_delegate + end + + # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. + def check_pending!(connection = Base.connection) + raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration? + end + + def load_schema_if_pending! + if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations? + # Roundtrip to Rake to allow plugins to hook into database initialization. + root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root + FileUtils.cd(root) do + current_config = Base.connection_config + Base.clear_all_connections! + system("bin/rails db:test:prepare") + # Establish a new connection, the old database may be gone (db:test:prepare uses purge) + Base.establish_connection(current_config) + end + check_pending! + end + end + + def maintain_test_schema! # :nodoc: + if ActiveRecord::Base.maintain_test_schema + suppress_messages { load_schema_if_pending! } + end + end + + def method_missing(name, *args, &block) # :nodoc: + nearest_delegate.send(name, *args, &block) + end + + def migrate(direction) + new.migrate direction + end + + # Disable the transaction wrapping this migration. + # You can still create your own transactions even after calling #disable_ddl_transaction! + # + # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration]. + def disable_ddl_transaction! + @disable_ddl_transaction = true + end + end + + def disable_ddl_transaction # :nodoc: + self.class.disable_ddl_transaction + end + + cattr_accessor :verbose + attr_accessor :name, :version + + def initialize(name = self.class.name, version = nil) + @name = name + @version = version + @connection = nil + end + + self.verbose = true + # instantiate the delegate object after initialize is defined + self.delegate = new + + # Reverses the migration commands for the given block and + # the given migrations. + # + # The following migration will remove the table 'horses' + # and create the table 'apples' on the way up, and the reverse + # on the way down. + # + # class FixTLMigration < ActiveRecord::Migration[5.0] + # def change + # revert do + # create_table(:horses) do |t| + # t.text :content + # t.datetime :remind_at + # end + # end + # create_table(:apples) do |t| + # t.string :variety + # end + # end + # end + # + # Or equivalently, if +TenderloveMigration+ is defined as in the + # documentation for Migration: + # + # require_relative '20121212123456_tenderlove_migration' + # + # class FixupTLMigration < ActiveRecord::Migration[5.0] + # def change + # revert TenderloveMigration + # + # create_table(:apples) do |t| + # t.string :variety + # end + # end + # end + # + # This command can be nested. + def revert(*migration_classes) + run(*migration_classes.reverse, revert: true) unless migration_classes.empty? + if block_given? + if connection.respond_to? :revert + connection.revert { yield } + else + recorder = command_recorder + @connection = recorder + suppress_messages do + connection.revert { yield } + end + @connection = recorder.delegate + recorder.replay(self) + end + end + end + + def reverting? + connection.respond_to?(:reverting) && connection.reverting + end + + ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc: + def up + yield unless reverting + end + + def down + yield if reverting + end + end + + # Used to specify an operation that can be run in one direction or another. + # Call the methods +up+ and +down+ of the yielded object to run a block + # only in one given direction. + # The whole block will be called in the right order within the migration. + # + # In the following example, the looping on users will always be done + # when the three columns 'first_name', 'last_name' and 'full_name' exist, + # even when migrating down: + # + # class SplitNameMigration < ActiveRecord::Migration[5.0] + # def change + # add_column :users, :first_name, :string + # add_column :users, :last_name, :string + # + # reversible do |dir| + # User.reset_column_information + # User.all.each do |u| + # dir.up { u.first_name, u.last_name = u.full_name.split(' ') } + # dir.down { u.full_name = "#{u.first_name} #{u.last_name}" } + # u.save + # end + # end + # + # revert { add_column :users, :full_name, :string } + # end + # end + def reversible + helper = ReversibleBlockHelper.new(reverting?) + execute_block { yield helper } + end + + # Used to specify an operation that is only run when migrating up + # (for example, populating a new column with its initial values). + # + # In the following example, the new column +published+ will be given + # the value +true+ for all existing records. + # + # class AddPublishedToPosts < ActiveRecord::Migration[5.2] + # def change + # add_column :posts, :published, :boolean, default: false + # up_only do + # execute "update posts set published = 'true'" + # end + # end + # end + def up_only + execute_block { yield } unless reverting? + end + + # Runs the given migration classes. + # Last argument can specify options: + # - :direction (default is :up) + # - :revert (default is false) + def run(*migration_classes) + opts = migration_classes.extract_options! + dir = opts[:direction] || :up + dir = (dir == :down ? :up : :down) if opts[:revert] + if reverting? + # If in revert and going :up, say, we want to execute :down without reverting, so + revert { run(*migration_classes, direction: dir, revert: true) } + else + migration_classes.each do |migration_class| + migration_class.new.exec_migration(connection, dir) + end + end + end + + def up + self.class.delegate = self + return unless self.class.respond_to?(:up) + self.class.up + end + + def down + self.class.delegate = self + return unless self.class.respond_to?(:down) + self.class.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 + + time = nil + ActiveRecord::Base.connection_pool.with_connection do |conn| + time = Benchmark.measure do + exec_migration(conn, direction) + end + end + + case direction + when :up then announce "migrated (%.4fs)" % time.real; write + when :down then announce "reverted (%.4fs)" % time.real; write + end + end + + def exec_migration(conn, direction) + @connection = conn + if respond_to?(:change) + if direction == :down + revert { change } + else + change + end + else + send(direction) + end + ensure + @connection = nil + 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 + + # Takes a message argument and outputs it as is. + # A second boolean argument can be passed to specify whether to indent or not. + def say(message, subitem = false) + write "#{subitem ? " ->" : "--"} #{message}" + end + + # Outputs text along with how long it took to run its block. + # If the block returns an integer it assumes it is the number of rows affected. + 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 + + # Takes a block as an argument and suppresses any output generated by the block. + def suppress_messages + save, self.verbose = verbose, false + yield + ensure + self.verbose = save + end + + def connection + @connection || ActiveRecord::Base.connection + end + + def method_missing(method, *arguments, &block) + arg_list = arguments.map(&:inspect) * ", " + + say_with_time "#{method}(#{arg_list})" do + unless connection.respond_to? :revert + unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) + arguments[0] = proper_table_name(arguments.first, table_name_options) + if [:rename_table, :add_foreign_key].include?(method) || + (method == :remove_foreign_key && !arguments.second.is_a?(Hash)) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end + end + end + return super unless connection.respond_to?(method) + connection.send(method, *arguments, &block) + end + end + + def copy(destination, sources, options = {}) + copied = [] + + FileUtils.mkdir_p(destination) unless File.exist?(destination) + + destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations + last = destination_migrations.last + sources.each do |scope, path| + source_migrations = ActiveRecord::MigrationContext.new(path).migrations + + source_migrations.each do |migration| + source = File.binread(migration.filename) + inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n" + magic_comments = +"" + loop do + # If we have a magic comment in the original migration, + # insert our comment after the first newline(end of the magic comment line) + # so the magic keep working. + # Note that magic comments must be at the first line(except sh-bang). + source.sub!(/\A(?:#.*\b(?:en)?coding:\s*\S+|#\s*frozen_string_literal:\s*(?:true|false)).*\n/) do |magic_comment| + magic_comments << magic_comment; "" + end || break + end + source = "#{magic_comments}#{inserted_comment}#{source}" + + if duplicate = destination_migrations.detect { |m| m.name == migration.name } + if options[:on_skip] && duplicate.scope != scope.to_s + options[:on_skip].call(scope, migration) + end + next + end + + migration.version = next_migration_number(last ? last.version + 1 : 0).to_i + new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb") + old_path, migration.filename = migration.filename, new_path + last = migration + + File.binwrite(migration.filename, source) + copied << migration + options[:on_copy].call(scope, migration, old_path) if options[:on_copy] + destination_migrations << migration + end + end + + copied + end + + # Finds the correct table name given an Active Record object. + # Uses the Active Record object's own table_name, or pre/suffix from the + # options passed in. + def proper_table_name(name, options = {}) + if name.respond_to? :table_name + name.table_name + else + "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" + end + end + + # Determines the version number of the next migration. + def next_migration_number(number) + if ActiveRecord::Base.timestamped_migrations + [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max + else + SchemaMigration.normalize_migration_number(number) + end + end + + # Builds a hash for use in ActiveRecord::Migration#proper_table_name using + # the Active Record object's table_name prefix and suffix + def table_name_options(config = ActiveRecord::Base) #:nodoc: + { + table_name_prefix: config.table_name_prefix, + table_name_suffix: config.table_name_suffix + } + end + + private + def execute_block + if connection.respond_to? :execute_block + super # use normal delegation to record the block + else + yield + end + end + + def command_recorder + CommandRecorder.new(connection) + end + end + + # MigrationProxy is used to defer loading of the actual migration classes + # until they are needed + MigrationProxy = Struct.new(:name, :version, :filename, :scope) do + def initialize(name, version, filename, scope) + super + @migration = nil + end + + def basename + File.basename(filename) + end + + def mtime + File.mtime filename + end + + delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration + + private + + def migration + @migration ||= load_migration + end + + def load_migration + require(File.expand_path(filename)) + name.constantize.new(name, version) + end + end + + class NullMigration < MigrationProxy #:nodoc: + def initialize + super(nil, 0, nil, nil) + end + + def mtime + 0 + end + end + + class MigrationContext # :nodoc: + attr_reader :migrations_paths + + def initialize(migrations_paths) + @migrations_paths = migrations_paths + end + + def migrate(target_version = nil, &block) + case + when target_version.nil? + up(target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(target_version, &block) + else + up(target_version, &block) + end + end + + def rollback(steps = 1) + move(:down, steps) + end + + def forward(steps = 1) + move(:up, steps) + end + + def up(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations + end + + Migrator.new(:up, selected_migrations, target_version).migrate + end + + def down(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations + end + + Migrator.new(:down, selected_migrations, target_version).migrate + end + + def run(direction, target_version) + Migrator.new(direction, migrations, target_version).run + end + + def open + Migrator.new(:up, migrations, nil) + end + + def get_all_versions + if SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] + end + end + + def current_version + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError + end + + def needs_migration? + (migrations.collect(&:version) - get_all_versions).size > 0 + end + + def any_migrations? + migrations.any? + end + + def last_migration #:nodoc: + migrations.last || NullMigration.new + end + + def migrations + migrations = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize + + MigrationProxy.new(name, version, file, scope) + end + + migrations.sort_by(&:version) + end + + def migrations_status + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact + + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] + end + + (db_list + file_list).sort_by { |_, version, _| version } + end + + def current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + + def protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end + + def last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? + + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end + + private + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end + + def parse_migration_filename(filename) + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + + def move(direction, steps) + migrator = Migrator.new(direction, migrations) + + if current_version != 0 && !migrator.current_migration + raise UnknownMigrationVersionError.new(current_version) + end + + start_index = + if current_version == 0 + 0 + else + migrator.migrations.index(migrator.current_migration) + end + + finish = migrator.migrations[start_index + steps] + version = finish ? finish.version : 0 + send(direction, version) + end + end + + class Migrator # :nodoc: + class << self + attr_accessor :migrations_paths + + def migrations_path=(path) + ActiveSupport::Deprecation.warn \ + "`ActiveRecord::Migrator.migrations_path=` is now deprecated and will be removed in Rails 6.0. " \ + "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." + self.migrations_paths = [path] + end + + # For cases where a table doesn't exist like loading from schema cache + def current_version + MigrationContext.new(migrations_paths).current_version + end + end + + self.migrations_paths = ["db/migrate"] + + def initialize(direction, migrations, target_version = nil) + @direction = direction + @target_version = target_version + @migrated_versions = nil + @migrations = migrations + + validate(@migrations) + + ActiveRecord::SchemaMigration.create_table + ActiveRecord::InternalMetadata.create_table + end + + def current_version + migrated.max || 0 + end + + def current_migration + migrations.detect { |m| m.version == current_version } + end + alias :current :current_migration + + def run + if use_advisory_lock? + with_advisory_lock { run_without_lock } + else + run_without_lock + end + end + + def migrate + if use_advisory_lock? + with_advisory_lock { migrate_without_lock } + else + migrate_without_lock + end + end + + def runnable + runnable = migrations[start..finish] + if up? + runnable.reject { |m| ran?(m) } + else + # skip the last migration if we're headed down, but not ALL the way down + runnable.pop if target + runnable.find_all { |m| ran?(m) } + end + end + + def migrations + down? ? @migrations.reverse : @migrations.sort_by(&:version) + end + + def pending_migrations + already_migrated = migrated + migrations.reject { |m| already_migrated.include?(m.version) } + end + + def migrated + @migrated_versions || load_migrated + end + + def load_migrated + @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions) + end + + private + + # Used for running a specific migration. + def run_without_lock + migration = migrations.detect { |m| m.version == @target_version } + raise UnknownMigrationVersionError.new(@target_version) if migration.nil? + result = execute_migration_in_transaction(migration, @direction) + + record_environment + result + end + + # Used for running multiple migrations up to or down to a certain value. + def migrate_without_lock + if invalid_target? + raise UnknownMigrationVersionError.new(@target_version) + end + + result = runnable.each do |migration| + execute_migration_in_transaction(migration, @direction) + end + + record_environment + result + end + + # Stores the current environment in the database. + def record_environment + return if down? + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment + end + + def ran?(migration) + migrated.include?(migration.version.to_i) + end + + # Return true if a valid version is not provided. + def invalid_target? + @target_version && @target_version != 0 && !target + end + + def execute_migration_in_transaction(migration, direction) + return if down? && !migrated.include?(migration.version.to_i) + return if up? && migrated.include?(migration.version.to_i) + + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger + + ddl_transaction(migration) do + migration.migrate(direction) + record_version_state_after_migrating(migration.version) + end + rescue => e + msg = +"An error has occurred, " + msg << "this and " if use_transaction?(migration) + msg << "all later migrations canceled:\n\n#{e}" + raise StandardError, msg, e.backtrace + end + + def target + migrations.detect { |m| m.version == @target_version } + end + + def finish + migrations.index(target) || migrations.size - 1 + end + + def start + up? ? 0 : (migrations.index(current) || 0) + end + + def validate(migrations) + name, = migrations.group_by(&:name).find { |_, v| v.length > 1 } + raise DuplicateMigrationNameError.new(name) if name + + version, = migrations.group_by(&:version).find { |_, v| v.length > 1 } + raise DuplicateMigrationVersionError.new(version) if version + end + + def record_version_state_after_migrating(version) + if down? + migrated.delete(version) + ActiveRecord::SchemaMigration.where(version: version.to_s).delete_all + else + migrated << version + ActiveRecord::SchemaMigration.create!(version: version.to_s) + end + end + + def up? + @direction == :up + end + + def down? + @direction == :down + end + + # Wrap the migration in a transaction only if supported by the adapter. + def ddl_transaction(migration) + if use_transaction?(migration) + Base.transaction { yield } + else + yield + end + end + + def use_transaction?(migration) + !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions? + end + + def use_advisory_lock? + Base.connection.advisory_locks_enabled? + end + + def with_advisory_lock + lock_id = generate_migrator_advisory_lock_id + connection = Base.connection + got_lock = connection.get_advisory_lock(lock_id) + raise ConcurrentMigrationError unless got_lock + load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock + yield + ensure + if got_lock && !connection.release_advisory_lock(lock_id) + raise ConcurrentMigrationError.new( + ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE + ) + end + end + + MIGRATOR_SALT = 2053462845 + def generate_migrator_advisory_lock_id + db_name_hash = Zlib.crc32(Base.connection.current_database) + MIGRATOR_SALT * db_name_hash + end + end +end |