From 8a161689b73485315763c8582d4d5ca0bcd1164e Mon Sep 17 00:00:00 2001 From: =?utf-8?q?Micha=C5=82=20=C5=81omnicki?= Date: Sat, 5 Feb 2011 20:01:38 +0100 Subject: [PATCH] postgresql support for bulk alter --- .../connection_adapters/abstract_adapter.rb | 6 ++ .../connection_adapters/mysql_adapter.rb | 4 + .../connection_adapters/postgresql_adapter.rb | 73 +++++++++++++++++--- activerecord/test/cases/migration_test.rb | 64 +++++++++-------- 4 files changed, 106 insertions(+), 41 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 3a3a73f..8e4b2f9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -82,6 +82,12 @@ module ActiveRecord false end + # Does this adapter support creating bulk indexes using ALTER TABLE? + # Only MySQL is known to support that feature. + def supports_bulk_indexes? + false + end + # Does this adapter support savepoints? PostgreSQL and MySQL do, # SQLite < 3.6.8 does not. def supports_savepoints? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index cdf1ebf..a7c1b2b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -207,6 +207,10 @@ module ActiveRecord true end + def supports_bulk_indexes? #:nodoc: + true + end + # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 46c0f3f..0b2423d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -211,6 +211,14 @@ module ActiveRecord ADAPTER_NAME end + def supports_bulk_alter? #:nodoc: + postgresql_version >= 80000 + end + + def supports_bulk_indexes? #:nodoc: + false + end + # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -842,17 +850,25 @@ module ActiveRecord execute "ALTER TABLE #{quote_table_name(name)} RENAME 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 + # Adds a new column to the named table. # See TableDefinition#column for details of the options you can use. def add_column(table_name, column_name, type, options = {}) - default = options[:default] - notnull = options[:null] == false - - # Add the column. - execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}") - - change_column_default(table_name, column_name, default) if options_include_default?(options) - change_column_null(table_name, column_name, false, default) if notnull + execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") end # Changes the column of a table. @@ -860,7 +876,7 @@ module ActiveRecord quoted_table_name = quote_table_name(table_name) begin - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + execute "ALTER TABLE #{quoted_table_name} #{change_column_sql(table_name, column_name, type, options)}" rescue ActiveRecord::StatementInvalid => e raise e if postgresql_version > 80000 # This is PostgreSQL 7.x, so we have to use a more arcane way of doing it. @@ -895,7 +911,7 @@ module ActiveRecord # Renames a column in a table. def rename_column(table_name, column_name, new_column_name) - execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + execute "ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}" end def remove_index!(table_name, index_name) #:nodoc: @@ -976,6 +992,43 @@ module ActiveRecord end end + def add_column_sql(table_name, column_name, type, options = {}) + add_column_sql = "ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_sql + end + + def change_column_sql(table_name, column_name, type, options = {}) + change_column_sql = "ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_alter_column_options!(change_column_sql, column_name, options) + change_column_sql + end + + def rename_column_sql(table_name, column_name, new_column_name) + "RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + end + + def add_alter_column_options!(sql, column_name, options) + quoted_column_name = quote_column_name(column_name) + sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quote(options[:default])}" if options_include_default?(options) + # must explicitly check for :null to allow change_column to work on migrations + if options.key?(:null) + sql << ", ALTER COLUMN #{quoted_column_name} #{options[:null] ? 'SET' : 'DROP'} NOT NULL" + end + 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 + + def remove_column_sql(table_name, *column_names) + columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }.join(', ') + end + private def exec_no_cache(sql, name) log(sql, name) do diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 9d7c497..e7a4b77 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1968,49 +1968,51 @@ if ActiveRecord::Base.connection.supports_migrations? 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 + if ActiveRecord::Base.connection.supports_bulk_indexes? + def test_adding_indexes with_bulk_change_table do |t| - t.index :username, :unique => true, :name => :awesome_username_index - t.index [:name, :age] + t.string :username + t.string :name + t.integer :age end - end - assert_equal 2, indexes.size + # 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 - 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_equal 2, indexes.size - assert index(:awesome_username_index).unique - end + 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 - def test_removing_index - with_bulk_change_table do |t| - t.string :name - t.index :name + assert index(:awesome_username_index).unique end - assert index(:index_delete_me_on_name) - - assert_queries(3) do + def test_removing_index with_bulk_change_table do |t| - t.remove_index :name - t.index :name, :name => :new_name_index, :unique => true + t.string :name + t.index :name end - end - assert ! index(:index_delete_me_on_name) + assert index(:index_delete_me_on_name) - new_name_index = index(:new_name_index) - assert new_name_index.unique + 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 end def test_changing_columns -- 1.6.3.3