Skip to content
This repository
Browse code

Add :bulk => true option to change_table

  • Loading branch information...
commit 30176f28a41681c7607eed39d03501327869d40c 1 parent 9db4c07
Pratik authored January 31, 2011
13  activerecord/CHANGELOG
... ...
@@ -1,5 +1,18 @@
1 1
 *Rails 3.1.0 (unreleased)*
2 2
 
  3
+* Add :bulk => true option to change_table to make all the schema changes defined in change_table block using a single ALTER statement. [Pratik Naik]
  4
+
  5
+  Example:
  6
+
  7
+  change_table(:users, :bulk => true) do |t|
  8
+    t.string :company_name
  9
+    t.change :birthdate, :datetime
  10
+  end
  11
+
  12
+  This will now result in:
  13
+
  14
+    ALTER TABLE `users` ADD COLUMN `company_name` varchar(255), CHANGE `updated_at` `updated_at` datetime DEFAULT NULL
  15
+
3 16
 * Removed support for accessing attributes on a has_and_belongs_to_many join table. This has been
4 17
   documented as deprecated behaviour since April 2006. Please use has_many :through instead.
5 18
   [Jon Leighton]
90  activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -176,6 +176,13 @@ def create_table(table_name, options = {})
176 176
       #    # Other column alterations here
177 177
       #  end
178 178
       #
  179
+      # The +options+ hash can include the following keys:
  180
+      # [<tt>:bulk</tt>]
  181
+      #   Set this to true to make this a bulk alter query, such as
  182
+      #   ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
  183
+      #
  184
+      #   Defaults to false.
  185
+      #
179 186
       # ===== Examples
180 187
       # ====== Add a column
181 188
       #  change_table(:suppliers) do |t|
@@ -224,8 +231,14 @@ def create_table(table_name, options = {})
224 231
       #
225 232
       # See also Table for details on
226 233
       # all of the various column transformation
227  
-      def change_table(table_name)
228  
-        yield Table.new(table_name, self)
  234
+      def change_table(table_name, options = {})
  235
+        if supports_bulk_alter? && options[:bulk]
  236
+          recorder = ActiveRecord::Migration::CommandRecorder.new(self)
  237
+          yield Table.new(table_name, recorder)
  238
+          bulk_change_table(table_name, recorder.commands)
  239
+        else
  240
+          yield Table.new(table_name, self)
  241
+        end
229 242
       end
230 243
 
231 244
       # Renames a table.
@@ -253,10 +266,7 @@ def add_column(table_name, column_name, type, options = {})
253 266
       #  remove_column(:suppliers, :qualification)
254 267
       #  remove_columns(:suppliers, :qualification, :experience)
255 268
       def remove_column(table_name, *column_names)
256  
-        raise ArgumentError.new("You must specify at least one column name.  Example: remove_column(:people, :first_name)") if column_names.empty?
257  
-        column_names.flatten.each do |column_name|
258  
-          execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
259  
-        end
  269
+        columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" }
260 270
       end
261 271
       alias :remove_columns :remove_column
262 272
 
@@ -327,25 +337,8 @@ def rename_column(table_name, column_name, new_column_name)
327 337
       #
328 338
       # Note: SQLite doesn't support index length
329 339
       def add_index(table_name, column_name, options = {})
330  
-        column_names = Array.wrap(column_name)
331  
-        index_name   = index_name(table_name, :column => column_names)
332  
-
333  
-        if Hash === options # legacy support, since this param was a string
334  
-          index_type = options[:unique] ? "UNIQUE" : ""
335  
-          index_name = options[:name].to_s if options.key?(:name)
336  
-        else
337  
-          index_type = options
338  
-        end
339  
-
340  
-        if index_name.length > index_name_length
341  
-          raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
342  
-        end
343  
-        if index_name_exists?(table_name, index_name, false)
344  
-          raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
345  
-        end
346  
-        quoted_column_names = quoted_columns_for_index(column_names, options).join(", ")
347  
-
348  
-        execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
  340
+        index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
  341
+        execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
349 342
       end
350 343
 
351 344
       # Remove the given index from the table.
@@ -359,11 +352,7 @@ def add_index(table_name, column_name, options = {})
359 352
       # Remove the index named by_branch_party in the accounts table.
360 353
       #   remove_index :accounts, :name => :by_branch_party
361 354
       def remove_index(table_name, options = {})
362  
-        index_name = index_name(table_name, options)
363  
-        unless index_name_exists?(table_name, index_name, true)
364  
-          raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
365  
-        end
366  
-        remove_index!(table_name, index_name)
  355
+        remove_index!(table_name, index_name_for_remove(table_name, options))
367 356
       end
368 357
 
369 358
       def remove_index!(table_name, index_name) #:nodoc:
@@ -469,7 +458,7 @@ def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migra
469 458
       end
470 459
 
471 460
       def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
472  
-        if native = native_database_types[type]
  461
+        if native = native_database_types[type.to_sym]
473 462
           column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
474 463
 
475 464
           if type == :decimal # ignore limit, use precision and scale
@@ -537,6 +526,45 @@ def options_include_default?(options)
537 526
           options.include?(:default) && !(options[:null] == false && options[:default].nil?)
538 527
         end
539 528
 
  529
+        def add_index_options(table_name, column_name, options = {})
  530
+          column_names = Array.wrap(column_name)
  531
+          index_name   = index_name(table_name, :column => column_names)
  532
+
  533
+          if Hash === options # legacy support, since this param was a string
  534
+            index_type = options[:unique] ? "UNIQUE" : ""
  535
+            index_name = options[:name].to_s if options.key?(:name)
  536
+          else
  537
+            index_type = options
  538
+          end
  539
+
  540
+          if index_name.length > index_name_length
  541
+            raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
  542
+          end
  543
+          if index_name_exists?(table_name, index_name, false)
  544
+            raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
  545
+          end
  546
+          index_columns = quoted_columns_for_index(column_names, options).join(", ")
  547
+
  548
+          [index_name, index_type, index_columns]
  549
+        end
  550
+
  551
+        def index_name_for_remove(table_name, options = {})
  552
+          index_name = index_name(table_name, options)
  553
+
  554
+          unless index_name_exists?(table_name, index_name, true)
  555
+            raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
  556
+          end
  557
+
  558
+          index_name
  559
+        end
  560
+
  561
+        def columns_for_remove(table_name, *column_names)
  562
+          column_names = column_names.flatten
  563
+
  564
+          raise ArgumentError.new("You must specify at least one column name.  Example: remove_column(:people, :first_name)") if column_names.blank?
  565
+          column_names.map {|column_name| quote_column_name(column_name) }
  566
+        end
  567
+
540 568
       private
541 569
       def table_definition
542 570
         TableDefinition.new(self)
4  activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -77,6 +77,10 @@ def supports_ddl_transactions?
77 77
         false
78 78
       end
79 79
 
  80
+      def supports_bulk_alter?
  81
+        false
  82
+      end
  83
+
80 84
       # Does this adapter support savepoints? PostgreSQL and MySQL do,
81 85
       # SQLite < 3.6.8 does not.
82 86
       def supports_savepoints?
114  activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -203,6 +203,10 @@ def adapter_name #:nodoc:
203 203
         ADAPTER_NAME
204 204
       end
205 205
 
  206
+      def supports_bulk_alter? #:nodoc:
  207
+        true
  208
+      end
  209
+
206 210
       # Returns +true+ when the connection adapter supports prepared statement
207 211
       # caching, otherwise returns +false+
208 212
       def supports_statement_cache?
@@ -547,11 +551,23 @@ def rename_table(table_name, new_name)
547 551
         execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
548 552
       end
549 553
 
  554
+      def bulk_change_table(table_name, operations) #:nodoc:
  555
+        sqls = operations.map do |command, args|
  556
+          table, arguments = args.shift, args
  557
+          method = :"#{command}_sql"
  558
+
  559
+          if respond_to?(method)
  560
+            send(method, table, *arguments)
  561
+          else
  562
+            raise "Unknown method called : #{method}(#{arguments.inspect})"
  563
+          end
  564
+        end.flatten.join(", ")
  565
+
  566
+        execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
  567
+      end
  568
+
550 569
       def add_column(table_name, column_name, type, options = {})
551  
-        add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
552  
-        add_column_options!(add_column_sql, options)
553  
-        add_column_position!(add_column_sql, options)
554  
-        execute(add_column_sql)
  570
+        execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
555 571
       end
556 572
 
557 573
       def change_column_default(table_name, column_name, default) #:nodoc:
@@ -570,34 +586,11 @@ def change_column_null(table_name, column_name, null, default = nil)
570 586
       end
571 587
 
572 588
       def change_column(table_name, column_name, type, options = {}) #:nodoc:
573  
-        column = column_for(table_name, column_name)
574  
-
575  
-        unless options_include_default?(options)
576  
-          options[:default] = column.default
577  
-        end
578  
-
579  
-        unless options.has_key?(:null)
580  
-          options[:null] = column.null
581  
-        end
582  
-
583  
-        change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
584  
-        add_column_options!(change_column_sql, options)
585  
-        add_column_position!(change_column_sql, options)
586  
-        execute(change_column_sql)
  589
+        execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
587 590
       end
588 591
 
589 592
       def rename_column(table_name, column_name, new_column_name) #:nodoc:
590  
-        options = {}
591  
-        if column = columns(table_name).find { |c| c.name == column_name.to_s }
592  
-          options[:default] = column.default
593  
-          options[:null] = column.null
594  
-        else
595  
-          raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
596  
-        end
597  
-        current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
598  
-        rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
599  
-        add_column_options!(rename_column_sql, options)
600  
-        execute(rename_column_sql)
  593
+        execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
601 594
       end
602 595
 
603 596
       # Maps logical Rails types to MySQL-specific data types.
@@ -680,6 +673,69 @@ def translate_exception(exception, message)
680 673
           end
681 674
         end
682 675
 
  676
+        def add_column_sql(table_name, column_name, type, options = {})
  677
+          add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
  678
+          add_column_options!(add_column_sql, options)
  679
+          add_column_position!(add_column_sql, options)
  680
+          add_column_sql
  681
+        end
  682
+
  683
+        def remove_column_sql(table_name, *column_names)
  684
+          columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
  685
+        end
  686
+        alias :remove_columns_sql :remove_column
  687
+
  688
+        def change_column_sql(table_name, column_name, type, options = {})
  689
+          column = column_for(table_name, column_name)
  690
+
  691
+          unless options_include_default?(options)
  692
+            options[:default] = column.default
  693
+          end
  694
+
  695
+          unless options.has_key?(:null)
  696
+            options[:null] = column.null
  697
+          end
  698
+
  699
+          change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
  700
+          add_column_options!(change_column_sql, options)
  701
+          add_column_position!(change_column_sql, options)
  702
+          change_column_sql
  703
+        end
  704
+
  705
+        def rename_column_sql(table_name, column_name, new_column_name)
  706
+          options = {}
  707
+
  708
+          if column = columns(table_name).find { |c| c.name == column_name.to_s }
  709
+            options[:default] = column.default
  710
+            options[:null] = column.null
  711
+          else
  712
+            raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
  713
+          end
  714
+
  715
+          current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
  716
+          rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
  717
+          add_column_options!(rename_column_sql, options)
  718
+          rename_column_sql
  719
+        end
  720
+
  721
+        def add_index_sql(table_name, column_name, options = {})
  722
+          index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
  723
+          "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
  724
+        end
  725
+
  726
+        def remove_index_sql(table_name, options = {})
  727
+          index_name = index_name_for_remove(table_name, options)
  728
+          "DROP INDEX #{index_name}"
  729
+        end
  730
+
  731
+        def add_timestamps_sql(table_name)
  732
+          [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
  733
+        end
  734
+
  735
+        def remove_timestamps_sql(table_name)
  736
+          [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
  737
+        end
  738
+
683 739
       private
684 740
         def connect
685 741
           encoding = @config[:encoding]
20  activerecord/lib/active_record/migration/command_recorder.rb
@@ -40,7 +40,7 @@ def inverse
40 40
         @commands.reverse.map { |name, args|
41 41
           method = :"invert_#{name}"
42 42
           raise IrreversibleMigration unless respond_to?(method, true)
43  
-          __send__(method, args)
  43
+          send(method, args)
44 44
         }
45 45
       end
46 46
 
@@ -48,12 +48,16 @@ def respond_to?(*args) # :nodoc:
48 48
         super || delegate.respond_to?(*args)
49 49
       end
50 50
 
51  
-      def send(method, *args) # :nodoc:
52  
-        return super unless respond_to?(method)
53  
-        record(method, args)
  51
+      [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
  52
+        class_eval <<-EOV, __FILE__, __LINE__ + 1
  53
+          def #{method}(*args)
  54
+            record(:"#{method}", args)
  55
+          end
  56
+        EOV
54 57
       end
55 58
 
56 59
       private
  60
+
57 61
       def invert_create_table(args)
58 62
         [:drop_table, args]
59 63
       end
@@ -86,6 +90,14 @@ def invert_remove_timestamps(args)
86 90
       def invert_add_timestamps(args)
87 91
         [:remove_timestamps, args]
88 92
       end
  93
+
  94
+      # Forwards any missing method call to the \target.
  95
+      def method_missing(method, *args, &block)
  96
+        @delegate.send(method, *args, &block)
  97
+      rescue NoMethodError => e
  98
+        raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
  99
+      end
  100
+
89 101
     end
90 102
   end
91 103
 end
2  activerecord/test/cases/migration/command_recorder_test.rb
@@ -16,7 +16,7 @@ def america; end
16 16
 
17 17
       def test_send_calls_super
18 18
         assert_raises(NoMethodError) do
19  
-          @recorder.send(:create_table, :horses)
  19
+          @recorder.send(:non_existing_method, :horses)
20 20
         end
21 21
       end
22 22
 
138  activerecord/test/cases/migration_test.rb
@@ -1923,6 +1923,144 @@ def with_change_table
1923 1923
     end
1924 1924
   end
1925 1925
 
  1926
+  class AlterTableMigrationsTest < ActiveRecord::TestCase
  1927
+    def setup
  1928
+      @connection = Person.connection
  1929
+      @connection.create_table(:delete_me, :force => true) {|t| }
  1930
+    end
  1931
+
  1932
+    def teardown
  1933
+      Person.connection.drop_table(:delete_me) rescue nil
  1934
+    end
  1935
+
  1936
+    def test_adding_multiple_columns
  1937
+      assert_queries(1) do
  1938
+        with_bulk_change_table do |t|
  1939
+          t.column :name, :string
  1940
+          t.string :qualification, :experience
  1941
+          t.integer :age, :default => 0
  1942
+          t.date :birthdate
  1943
+          t.timestamps
  1944
+        end
  1945
+      end
  1946
+
  1947
+      assert_equal 8, columns.size
  1948
+      [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
  1949
+      assert_equal 0, column(:age).default
  1950
+    end
  1951
+
  1952
+    def test_removing_columns
  1953
+      with_bulk_change_table do |t|
  1954
+        t.string :qualification, :experience
  1955
+      end
  1956
+
  1957
+      [:qualification, :experience].each {|c| assert column(c) }
  1958
+
  1959
+      assert_queries(1) do
  1960
+        with_bulk_change_table do |t|
  1961
+          t.remove :qualification, :experience
  1962
+          t.string :qualification_experience
  1963
+        end
  1964
+      end
  1965
+
  1966
+      [:qualification, :experience].each {|c| assert ! column(c) }
  1967
+      assert column(:qualification_experience)
  1968
+    end
  1969
+
  1970
+    def test_adding_indexes
  1971
+      with_bulk_change_table do |t|
  1972
+        t.string :username
  1973
+        t.string :name
  1974
+        t.integer :age
  1975
+      end
  1976
+
  1977
+      # Adding an index fires a query everytime to check if an index already exists or not
  1978
+      assert_queries(3) do
  1979
+        with_bulk_change_table do |t|
  1980
+          t.index :username, :unique => true, :name => :awesome_username_index
  1981
+          t.index [:name, :age]
  1982
+        end
  1983
+      end
  1984
+
  1985
+      assert_equal 2, indexes.size
  1986
+
  1987
+      name_age_index = index(:index_delete_me_on_name_and_age)
  1988
+      assert_equal ['name', 'age'].sort, name_age_index.columns.sort
  1989
+      assert ! name_age_index.unique
  1990
+
  1991
+      assert index(:awesome_username_index).unique
  1992
+    end
  1993
+
  1994
+    def test_removing_index
  1995
+      with_bulk_change_table do |t|
  1996
+        t.string :name
  1997
+        t.index :name
  1998
+      end
  1999
+
  2000
+      assert index(:index_delete_me_on_name)
  2001
+
  2002
+      assert_queries(3) do
  2003
+        with_bulk_change_table do |t|
  2004
+          t.remove_index :name
  2005
+          t.index :name, :name => :new_name_index, :unique => true
  2006
+        end
  2007
+      end
  2008
+
  2009
+      assert ! index(:index_delete_me_on_name)
  2010
+
  2011
+      new_name_index = index(:new_name_index)
  2012
+      assert new_name_index.unique
  2013
+    end
  2014
+
  2015
+    def test_changing_columns
  2016
+      with_bulk_change_table do |t|
  2017
+        t.string :name
  2018
+        t.date :birthdate
  2019
+      end
  2020
+
  2021
+      assert ! column(:name).default
  2022
+      assert_equal :date, column(:birthdate).type
  2023
+
  2024
+      assert_queries(1) do
  2025
+        with_bulk_change_table do |t|
  2026
+          t.change :name, :string, :default => 'NONAME'
  2027
+          t.change :birthdate, :datetime
  2028
+        end
  2029
+      end
  2030
+
  2031
+      assert_equal 'NONAME', column(:name).default
  2032
+      assert_equal :datetime, column(:birthdate).type
  2033
+    end
  2034
+
  2035
+    protected
  2036
+
  2037
+    def with_bulk_change_table
  2038
+      # Reset columns/indexes cache as we're changing the table
  2039
+      @columns = @indexes = nil
  2040
+
  2041
+      Person.connection.change_table(:delete_me, :bulk => true) do |t|
  2042
+        yield t
  2043
+      end
  2044
+    end
  2045
+
  2046
+    def column(name)
  2047
+      columns.detect {|c| c.name == name.to_s }
  2048
+    end
  2049
+
  2050
+    def columns
  2051
+      @columns ||= Person.connection.columns('delete_me')
  2052
+    end
  2053
+
  2054
+    def index(name)
  2055
+      indexes.detect {|i| i.name == name.to_s }
  2056
+    end
  2057
+
  2058
+    def indexes
  2059
+      @indexes ||= Person.connection.indexes('delete_me')
  2060
+    end
  2061
+
  2062
+  end
  2063
+
1926 2064
   class CopyMigrationsTest < ActiveRecord::TestCase
1927 2065
     def setup
1928 2066
     end

0 notes on commit 30176f2

Aaron Lasseigne

The :bulk option is ignored if the database adapter doesn't support it so why not default this to true?

Please sign in to comment.
Something went wrong with that request. Please try again.