Permalink
Browse files

Add :bulk => true option to change_table

  • Loading branch information...
1 parent 9db4c07 commit 30176f28a41681c7607eed39d03501327869d40c @lifo lifo committed Jan 31, 2011
View
13 activerecord/CHANGELOG
@@ -1,5 +1,18 @@
*Rails 3.1.0 (unreleased)*
+* 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]
+
+ Example:
+
+ change_table(:users, :bulk => true) do |t|
+ t.string :company_name
+ t.change :birthdate, :datetime
+ end
+
+ This will now result in:
+
+ ALTER TABLE `users` ADD COLUMN `company_name` varchar(255), CHANGE `updated_at` `updated_at` datetime DEFAULT NULL
+
* Removed support for accessing attributes on a has_and_belongs_to_many join table. This has been
documented as deprecated behaviour since April 2006. Please use has_many :through instead.
[Jon Leighton]
View
90 activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -176,6 +176,13 @@ def create_table(table_name, options = {})
# # Other column alterations here
# end
#
+ # The +options+ hash can include the following keys:
+ # [<tt>:bulk</tt>]
+ # Set this to true to make this a bulk alter query, such as
+ # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ #
+ # Defaults to false.
@AaronLasseigne
AaronLasseigne May 17, 2012

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

+ #
# ===== Examples
# ====== Add a column
# change_table(:suppliers) do |t|
@@ -224,8 +231,14 @@ def create_table(table_name, options = {})
#
# See also Table for details on
# all of the various column transformation
- def change_table(table_name)
- yield Table.new(table_name, self)
+ def change_table(table_name, options = {})
+ if supports_bulk_alter? && options[:bulk]
+ recorder = ActiveRecord::Migration::CommandRecorder.new(self)
+ yield Table.new(table_name, recorder)
+ bulk_change_table(table_name, recorder.commands)
+ else
+ yield Table.new(table_name, self)
+ end
end
# Renames a table.
@@ -253,10 +266,7 @@ def add_column(table_name, column_name, type, options = {})
# remove_column(:suppliers, :qualification)
# remove_columns(:suppliers, :qualification, :experience)
def remove_column(table_name, *column_names)
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
- column_names.flatten.each do |column_name|
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
- end
+ columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" }
end
alias :remove_columns :remove_column
@@ -327,25 +337,8 @@ def rename_column(table_name, column_name, new_column_name)
#
# Note: SQLite doesn't support index length
def add_index(table_name, column_name, options = {})
- column_names = Array.wrap(column_name)
- index_name = index_name(table_name, :column => column_names)
-
- if Hash === options # legacy support, since this param was a string
- index_type = options[:unique] ? "UNIQUE" : ""
- index_name = options[:name].to_s if options.key?(:name)
- else
- index_type = options
- end
-
- if index_name.length > index_name_length
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
- end
- if index_name_exists?(table_name, index_name, false)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
- end
- quoted_column_names = quoted_columns_for_index(column_names, options).join(", ")
-
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
end
# Remove the given index from the table.
@@ -359,11 +352,7 @@ def add_index(table_name, column_name, options = {})
# Remove the index named by_branch_party in the accounts table.
# remove_index :accounts, :name => :by_branch_party
def remove_index(table_name, options = {})
- index_name = index_name(table_name, options)
- unless index_name_exists?(table_name, index_name, true)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
- end
- remove_index!(table_name, index_name)
+ remove_index!(table_name, index_name_for_remove(table_name, options))
end
def remove_index!(table_name, index_name) #:nodoc:
@@ -469,7 +458,7 @@ def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migra
end
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
- if native = native_database_types[type]
+ if native = native_database_types[type.to_sym]
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
if type == :decimal # ignore limit, use precision and scale
@@ -537,6 +526,45 @@ def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+ def add_index_options(table_name, column_name, options = {})
+ column_names = Array.wrap(column_name)
+ index_name = index_name(table_name, :column => column_names)
+
+ if Hash === options # legacy support, since this param was a string
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_name = options[:name].to_s if options.key?(:name)
+ else
+ index_type = options
+ end
+
+ if index_name.length > index_name_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
+ end
+ if index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns]
+ end
+
+ def index_name_for_remove(table_name, options = {})
+ index_name = index_name(table_name, options)
+
+ unless index_name_exists?(table_name, index_name, true)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ end
+
+ index_name
+ end
+
+ def columns_for_remove(table_name, *column_names)
+ column_names = column_names.flatten
+
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
+ column_names.map {|column_name| quote_column_name(column_name) }
+ end
+
private
def table_definition
TableDefinition.new(self)
View
4 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -77,6 +77,10 @@ def supports_ddl_transactions?
false
end
+ def supports_bulk_alter?
+ false
+ end
+
# Does this adapter support savepoints? PostgreSQL and MySQL do,
# SQLite < 3.6.8 does not.
def supports_savepoints?
View
114 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -203,6 +203,10 @@ def adapter_name #:nodoc:
ADAPTER_NAME
end
+ def supports_bulk_alter? #:nodoc:
+ true
+ end
+
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
def supports_statement_cache?
@@ -547,11 +551,23 @@ def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
+ def bulk_change_table(table_name, operations) #:nodoc:
+ sqls = operations.map do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_sql"
+
+ if respond_to?(method)
+ send(method, table, *arguments)
+ else
+ raise "Unknown method called : #{method}(#{arguments.inspect})"
+ end
+ end.flatten.join(", ")
+
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
+ end
+
def add_column(table_name, column_name, type, options = {})
- 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])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- execute(add_column_sql)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
end
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)
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
- column = column_for(table_name, column_name)
-
- unless options_include_default?(options)
- options[:default] = column.default
- end
-
- unless options.has_key?(:null)
- options[:null] = column.null
- end
-
- 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])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- execute(change_column_sql)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- options = {}
- if column = columns(table_name).find { |c| c.name == column_name.to_s }
- options[:default] = column.default
- options[:null] = column.null
- else
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
- end
- current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
- rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- execute(rename_column_sql)
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
end
# Maps logical Rails types to MySQL-specific data types.
@@ -680,6 +673,69 @@ def translate_exception(exception, message)
end
end
+ def add_column_sql(table_name, column_name, type, options = {})
+ add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(add_column_sql, options)
+ add_column_position!(add_column_sql, options)
+ add_column_sql
+ end
+
+ def remove_column_sql(table_name, *column_names)
+ columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
+ end
+ alias :remove_columns_sql :remove_column
+
+ def change_column_sql(table_name, column_name, type, options = {})
+ column = column_for(table_name, column_name)
+
+ unless options_include_default?(options)
+ options[:default] = column.default
+ end
+
+ unless options.has_key?(:null)
+ options[:null] = column.null
+ end
+
+ change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(change_column_sql, options)
+ add_column_position!(change_column_sql, options)
+ change_column_sql
+ end
+
+ def rename_column_sql(table_name, column_name, new_column_name)
+ options = {}
+
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
+ options[:default] = column.default
+ options[:null] = column.null
+ else
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
+ end
+
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
+ rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
+ add_column_options!(rename_column_sql, options)
+ rename_column_sql
+ end
+
+ def add_index_sql(table_name, column_name, options = {})
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
+ "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
+ end
+
+ def remove_index_sql(table_name, options = {})
+ index_name = index_name_for_remove(table_name, options)
+ "DROP INDEX #{index_name}"
+ end
+
+ def add_timestamps_sql(table_name)
+ [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ end
+
+ def remove_timestamps_sql(table_name)
+ [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
+ end
+
private
def connect
encoding = @config[:encoding]
View
20 activerecord/lib/active_record/migration/command_recorder.rb
@@ -40,20 +40,24 @@ def inverse
@commands.reverse.map { |name, args|
method = :"invert_#{name}"
raise IrreversibleMigration unless respond_to?(method, true)
- __send__(method, args)
+ send(method, args)
}
end
def respond_to?(*args) # :nodoc:
super || delegate.respond_to?(*args)
end
- def send(method, *args) # :nodoc:
- return super unless respond_to?(method)
- record(method, args)
+ [: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|
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
+ def #{method}(*args)
+ record(:"#{method}", args)
+ end
+ EOV
end
private
+
def invert_create_table(args)
[:drop_table, args]
end
@@ -86,6 +90,14 @@ def invert_remove_timestamps(args)
def invert_add_timestamps(args)
[:remove_timestamps, args]
end
+
+ # Forwards any missing method call to the \target.
+ def method_missing(method, *args, &block)
+ @delegate.send(method, *args, &block)
+ rescue NoMethodError => e
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
+ end
+
end
end
end
View
2 activerecord/test/cases/migration/command_recorder_test.rb
@@ -16,7 +16,7 @@ def america; end
def test_send_calls_super
assert_raises(NoMethodError) do
- @recorder.send(:create_table, :horses)
+ @recorder.send(:non_existing_method, :horses)
end
end
View
138 activerecord/test/cases/migration_test.rb
@@ -1923,6 +1923,144 @@ def with_change_table
end
end
+ class AlterTableMigrationsTest < ActiveRecord::TestCase
+ def setup
+ @connection = Person.connection
+ @connection.create_table(:delete_me, :force => true) {|t| }
+ end
+
+ def teardown
+ Person.connection.drop_table(:delete_me) rescue nil
+ end
+
+ def test_adding_multiple_columns
+ assert_queries(1) do
+ with_bulk_change_table do |t|
+ t.column :name, :string
+ t.string :qualification, :experience
+ t.integer :age, :default => 0
+ t.date :birthdate
+ t.timestamps
+ end
+ end
+
+ assert_equal 8, columns.size
+ [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
+ assert_equal 0, column(:age).default
+ end
+
+ def test_removing_columns
+ with_bulk_change_table do |t|
+ t.string :qualification, :experience
+ end
+
+ [:qualification, :experience].each {|c| assert column(c) }
+
+ assert_queries(1) do
+ with_bulk_change_table do |t|
+ t.remove :qualification, :experience
+ t.string :qualification_experience
+ end
+ end
+
+ [:qualification, :experience].each {|c| assert ! column(c) }
+ assert column(:qualification_experience)
+ end
+
+ def test_adding_indexes
+ with_bulk_change_table do |t|
+ t.string :username
+ t.string :name
+ t.integer :age
+ end
+
+ # Adding an index fires a query everytime to check if an index already exists or not
+ assert_queries(3) do
+ with_bulk_change_table do |t|
+ t.index :username, :unique => true, :name => :awesome_username_index
+ t.index [:name, :age]
+ end
+ end
+
+ assert_equal 2, indexes.size
+
+ name_age_index = index(:index_delete_me_on_name_and_age)
+ assert_equal ['name', 'age'].sort, name_age_index.columns.sort
+ assert ! name_age_index.unique
+
+ assert index(:awesome_username_index).unique
+ end
+
+ def test_removing_index
+ with_bulk_change_table do |t|
+ t.string :name
+ t.index :name
+ end
+
+ assert index(:index_delete_me_on_name)
+
+ assert_queries(3) do
+ with_bulk_change_table do |t|
+ t.remove_index :name
+ t.index :name, :name => :new_name_index, :unique => true
+ end
+ end
+
+ assert ! index(:index_delete_me_on_name)
+
+ new_name_index = index(:new_name_index)
+ assert new_name_index.unique
+ end
+
+ def test_changing_columns
+ with_bulk_change_table do |t|
+ t.string :name
+ t.date :birthdate
+ end
+
+ assert ! column(:name).default
+ assert_equal :date, column(:birthdate).type
+
+ assert_queries(1) do
+ with_bulk_change_table do |t|
+ t.change :name, :string, :default => 'NONAME'
+ t.change :birthdate, :datetime
+ end
+ end
+
+ assert_equal 'NONAME', column(:name).default
+ assert_equal :datetime, column(:birthdate).type
+ end
+
+ protected
+
+ def with_bulk_change_table
+ # Reset columns/indexes cache as we're changing the table
+ @columns = @indexes = nil
+
+ Person.connection.change_table(:delete_me, :bulk => true) do |t|
+ yield t
+ end
+ end
+
+ def column(name)
+ columns.detect {|c| c.name == name.to_s }
+ end
+
+ def columns
+ @columns ||= Person.connection.columns('delete_me')
+ end
+
+ def index(name)
+ indexes.detect {|i| i.name == name.to_s }
+ end
+
+ def indexes
+ @indexes ||= Person.connection.indexes('delete_me')
+ end
+
+ end
+
class CopyMigrationsTest < ActiveRecord::TestCase
def setup
end

0 comments on commit 30176f2

Please sign in to comment.