Add bulk alter support for PostgreSQL

This commit is contained in:
Dinah Shi 2017-12-06 18:09:24 +08:00
parent 07788c7ad8
commit 8dd4868cc1
4 changed files with 68 additions and 13 deletions

View File

@ -364,6 +364,31 @@ module ActiveRecord
SQL
end
def bulk_change_table(table_name, operations)
sql_fragments = []
non_combinable_operations = []
operations.each do |command, args|
table, arguments = args.shift, args
method = :"#{command}_for_alter"
if respond_to?(method, true)
sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
sql_fragments << sqls
non_combinable_operations << procs if procs.present?
else
execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
non_combinable_operations.each(&:call)
sql_fragments = []
non_combinable_operations = []
send(command, table, *arguments)
end
end
execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
non_combinable_operations.each(&:call)
end
# Renames a table.
# Also renames a table's primary key sequence if the sequence name exists and
# matches the Active Record default.
@ -394,7 +419,7 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
sqls, procs = change_column_for_alter(table_name, column_name, type, options)
sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) }
execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}"
procs.each(&:call)
end
@ -665,12 +690,10 @@ module ActiveRecord
def change_column_for_alter(table_name, column_name, type, options = {})
sqls = [change_column_sql(table_name, column_name, type, options)]
procs = []
sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
procs << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
[sqls, procs]
sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
sqls
end
@ -694,6 +717,14 @@ module ActiveRecord
"ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
end
def add_timestamps_for_alter(table_name, options = {})
[add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
end
def remove_timestamps_for_alter(table_name, options = {})
[remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
end
def add_index_opclass(quoted_columns, **options)
opclasses = options_for_index_columns(options[:opclass])
quoted_columns.each do |name, column|

View File

@ -122,6 +122,10 @@ module ActiveRecord
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
def supports_bulk_alter?
true
end
def supports_index_sort_order?
true
end

View File

@ -801,8 +801,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.integer :age
end
# Adding an index fires a query every time to check if an index already exists or not
assert_queries(3) do
classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
expected_query_count = {
"Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
"PostgreSQLAdapter" => 2,
}.fetch(classname) {
raise "need an expected query count for #{classname}"
}
assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.index :username, unique: true, name: :awesome_username_index
t.index [:name, :age]
@ -826,7 +833,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert index(:index_delete_me_on_name)
assert_queries(3) do
classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
expected_query_count = {
"Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
"PostgreSQLAdapter" => 2,
}.fetch(classname) {
raise "need an expected query count for #{classname}"
}
assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.remove_index :name
t.index :name, name: :new_name_index, unique: true
@ -848,10 +863,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert ! column(:name).default
assert_equal :date, column(:birthdate).type
# One query for columns (delete_me table)
# One query for primary key (delete_me table)
# One query to do the bulk change
assert_queries(3, ignore_none: true) do
classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
expected_query_count = {
"Mysql2Adapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change
"PostgreSQLAdapter" => 2, # one query for columns, one for bulk change
}.fetch(classname) {
raise "need an expected query count for #{classname}"
}
assert_queries(expected_query_count, ignore_none: true) do
with_bulk_change_table do |t|
t.change :name, :string, default: "NONAME"
t.change :birthdate, :datetime

View File

@ -112,7 +112,7 @@ module ActiveRecord
# instead examining the SQL content.
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im]
mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|