Skip to content

Commit

Permalink
Add :bulk => true option to change_table
Browse files Browse the repository at this point in the history
  • Loading branch information
lifo committed Jan 31, 2011
1 parent 9db4c07 commit 30176f2
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 65 deletions.
13 changes: 13 additions & 0 deletions 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]
Expand Down
Expand Up @@ -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.

This comment has been minimized.

Copy link
@AaronLasseigne

AaronLasseigne May 17, 2012

Contributor

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|
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Expand Up @@ -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?
Expand Down
114 changes: 85 additions & 29 deletions activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
Expand Up @@ -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?
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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]
Expand Down
20 changes: 16 additions & 4 deletions activerecord/lib/active_record/migration/command_recorder.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion activerecord/test/cases/migration/command_recorder_test.rb
Expand Up @@ -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

Expand Down

0 comments on commit 30176f2

Please sign in to comment.