Add :bulk => true option to change_table
This commit is contained in:
parent
9db4c07e0b
commit
30176f28a4
|
@ -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]
|
||||
|
|
|
@ -176,6 +176,13 @@ module ActiveRecord
|
|||
# # 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.
|
||||
#
|
||||
# ===== Examples
|
||||
# ====== Add a column
|
||||
# change_table(:suppliers) do |t|
|
||||
|
@ -224,8 +231,14 @@ module ActiveRecord
|
|||
#
|
||||
# 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 @@ module ActiveRecord
|
|||
# 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 @@ module ActiveRecord
|
|||
#
|
||||
# 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 @@ module ActiveRecord
|
|||
# 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 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
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)
|
||||
|
|
|
@ -77,6 +77,10 @@ module ActiveRecord
|
|||
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?
|
||||
|
|
|
@ -203,6 +203,10 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
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]
|
||||
|
|
|
@ -40,7 +40,7 @@ module ActiveRecord
|
|||
@commands.reverse.map { |name, args|
|
||||
method = :"invert_#{name}"
|
||||
raise IrreversibleMigration unless respond_to?(method, true)
|
||||
__send__(method, args)
|
||||
send(method, args)
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -48,12 +48,16 @@ module ActiveRecord
|
|||
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 @@ module ActiveRecord
|
|||
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
|
||||
|
|
|
@ -16,7 +16,7 @@ module ActiveRecord
|
|||
|
||||
def test_send_calls_super
|
||||
assert_raises(NoMethodError) do
|
||||
@recorder.send(:create_table, :horses)
|
||||
@recorder.send(:non_existing_method, :horses)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1923,6 +1923,144 @@ if ActiveRecord::Base.connection.supports_migrations?
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue