command_recorder.rb 8.4 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
module ActiveRecord
  class Migration
V
Vijay Dev 已提交
5
    # <tt>ActiveRecord::Migration::CommandRecorder</tt> records commands done during
6
    # a migration and knows how to reverse those commands. The CommandRecorder
7 8 9
    # knows how to invert the following commands:
    #
    # * add_column
10
    # * add_foreign_key
11
    # * add_index
12
    # * add_reference
V
Vijay Dev 已提交
13
    # * add_timestamps
14
    # * change_column
15 16
    # * change_column_default (must supply a :from and :to option)
    # * change_column_null
17
    # * create_join_table
18 19 20 21 22 23
    # * create_table
    # * disable_extension
    # * drop_join_table
    # * drop_table (must supply a block)
    # * enable_extension
    # * remove_column (must supply a type)
24
    # * remove_columns (must specify at least one column name or more)
25 26 27
    # * remove_foreign_key (must supply a second table)
    # * remove_index
    # * remove_reference
28 29 30 31
    # * remove_timestamps
    # * rename_column
    # * rename_index
    # * rename_table
32
    class CommandRecorder
Y
yui-knk 已提交
33 34 35
      ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
        :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
        :change_column_default, :add_reference, :remove_reference, :transaction,
36
        :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension,
Y
yui-knk 已提交
37 38 39
        :change_column, :execute, :remove_columns, :change_column_null,
        :add_foreign_key, :remove_foreign_key
      ]
40 41
      include JoinTable

42
      attr_accessor :commands, :delegate, :reverting
43

44
      def initialize(delegate = nil)
45
        @commands = []
46
        @delegate = delegate
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
        @reverting = false
      end

      # While executing the given block, the recorded will be in reverting mode.
      # All commands recorded will end up being recorded reverted
      # and in reverse order.
      # For example:
      #
      #   recorder.revert{ recorder.record(:rename_table, [:old, :new]) }
      #   # same effect as recorder.record(:rename_table, [:new, :old])
      def revert
        @reverting = !@reverting
        previous = @commands
        @commands = []
        yield
      ensure
        @commands = previous.concat(@commands.reverse)
        @reverting = !@reverting
65 66
      end

Y
yui-knk 已提交
67
      # Record +command+. +command+ should be a method name and arguments.
68 69
      # For example:
      #
V
Vijay Dev 已提交
70
      #   recorder.record(:method_name, [:arg1, :arg2])
71 72 73 74 75 76
      def record(*command, &block)
        if @reverting
          @commands << inverse_of(*command, &block)
        else
          @commands << (command << block)
        end
77 78
      end

79
      # Returns the inverse of the given command. For example:
A
Aaron Patterson 已提交
80
      #
81 82
      #   recorder.inverse_of(:rename_table, [:old, :new])
      #   # => [:rename_table, [:new, :old]]
A
Aaron Patterson 已提交
83
      #
V
Vijay Dev 已提交
84
      # This method will raise an +IrreversibleMigration+ exception if it cannot
85 86 87
      # invert the +command+.
      def inverse_of(command, args, &block)
        method = :"invert_#{command}"
88
        raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true)
89 90 91 92
          This migration uses #{command}, which is not automatically reversible.
          To make the migration reversible you can either:
          1. Define #up and #down methods in place of the #change method.
          2. Use the #reversible method to define reversible behavior.
93
        MSG
94
        send(method, args, &block)
95 96
      end

Y
yui-knk 已提交
97
      ReversibleAndIrreversibleMethods.each do |method|
98
        class_eval <<-EOV, __FILE__, __LINE__ + 1
99 100 101
          def #{method}(*args, &block)          # def create_table(*args, &block)
            record(:"#{method}", args, &block)  #   record(:create_table, args, &block)
          end                                   # end
102
        EOV
103
      end
104 105
      alias :add_belongs_to :add_reference
      alias :remove_belongs_to :remove_reference
106

T
Tom Kadwill 已提交
107
      def change_table(table_name, options = {}) # :nodoc:
108
        yield delegate.update_table_definition(table_name, self)
109 110
      end

111
      private
112

113 114 115 116 117 118 119 120 121 122 123 124 125
        module StraightReversions
          private
            { transaction:       :transaction,
              execute_block:     :execute_block,
              create_table:      :drop_table,
              create_join_table: :drop_join_table,
              add_column:        :remove_column,
              add_timestamps:    :remove_timestamps,
              add_reference:     :remove_reference,
              enable_extension:  :disable_extension
            }.each do |cmd, inv|
              [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
                class_eval <<-EOV, __FILE__, __LINE__ + 1
126 127 128 129
                  def invert_#{method}(args, &block)    # def invert_create_table(args, &block)
                    [:#{inverse}, args, block]          #   [:drop_table, args, block]
                  end                                   # end
                EOV
130 131
              end
            end
132
        end
133

134
        include StraightReversions
135

136 137 138 139 140
        def invert_drop_table(args, &block)
          if args.size == 1 && block == nil
            raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)."
          end
          super
141
        end
142

143 144 145
        def invert_rename_table(args)
          [:rename_table, args.reverse]
        end
146

147 148 149 150
        def invert_remove_column(args)
          raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
          super
        end
151

152 153 154
        def invert_rename_index(args)
          [:rename_index, [args.first] + args.last(2).reverse]
        end
155

156 157 158
        def invert_rename_column(args)
          [:rename_column, [args.first] + args.last(2).reverse]
        end
A
Aaron Patterson 已提交
159

160 161 162
        def invert_add_index(args)
          table, columns, options = *args
          options ||= {}
163

164 165
          options_hash = options.slice(:name, :algorithm)
          options_hash[:column] = columns if !options_hash[:name]
166

167 168
          [:remove_index, [table, options_hash]]
        end
169

170 171 172 173 174 175 176 177 178 179
        def invert_remove_index(args)
          table, options_or_column = *args
          if (options = options_or_column).is_a?(Hash)
            unless options[:column]
              raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
            end
            options = options.dup
            [:add_index, [table, options.delete(:column), options]]
          elsif (column = options_or_column).present?
            [:add_index, [table, column]]
180
          end
181
        end
182

183 184 185 186 187
        alias :invert_add_belongs_to :invert_add_reference
        alias :invert_remove_belongs_to :invert_remove_reference

        def invert_change_column_default(args)
          table, column, options = *args
188

189 190 191
          unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to)
            raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option."
          end
192

193
          [:change_column_default, [table, column, from: options[:to], to: options[:from]]]
194 195
        end

196 197 198 199
        def invert_change_column_null(args)
          args[2] = !args[2]
          [:change_column_null, args]
        end
200

201 202 203
        def invert_add_foreign_key(args)
          from_table, to_table, add_options = args
          add_options ||= {}
204

205 206 207 208 209 210 211
          if add_options[:name]
            options = { name: add_options[:name] }
          elsif add_options[:column]
            options = { column: add_options[:column] }
          else
            options = to_table
          end
212

213
          [:remove_foreign_key, [from_table, options]]
214 215
        end

216 217 218
        def invert_remove_foreign_key(args)
          from_table, to_table, remove_options = args
          raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash)
219

220 221
          reversed_args = [from_table, to_table]
          reversed_args << remove_options if remove_options
222

223 224
          [:add_foreign_key, reversed_args]
        end
225

226 227 228 229
        def respond_to_missing?(method, _)
          super || delegate.respond_to?(method)
        end

230
        # Forwards any missing method call to the \target.
231
        def method_missing(method, *args, &block)
232 233
          if delegate.respond_to?(method)
            delegate.public_send(method, *args, &block)
234 235 236
          else
            super
          end
237
        end
238 239 240
    end
  end
end