From f1880cac5862172608ff26d1178a31c05b904d77 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 5 Jul 2005 07:19:20 +0000 Subject: Sugared up migrations with even more bling #1609 [Tobias Luekte] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1697 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- .../connection_adapters/abstract_adapter.rb | 96 +++++++++++++++++----- .../connection_adapters/mysql_adapter.rb | 15 +++- .../connection_adapters/postgresql_adapter.rb | 18 +++- activerecord/lib/active_record/migration.rb | 17 +++- .../fixtures/migrations/3_innocent_jointable.rb | 12 +++ activerecord/test/migration_test.rb | 63 ++++++++++++-- 6 files changed, 185 insertions(+), 36 deletions(-) create mode 100644 activerecord/test/fixtures/migrations/3_innocent_jointable.rb diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 1b5c8184ae..e335ab4e07 100755 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -366,11 +366,17 @@ module ActiveRecord # Schema has been intialized end end - - def create_table(name, options = "") - execute "CREATE TABLE #{name} (id #{native_database_types[:primary_key]}) #{options}" - table_definition = yield TableDefinition.new - table_definition.columns.each { |column_name, type, options| add_column(name, column_name, type, options) } + + def create_table(name, options = {}) + table_definition = TableDefinition.new(self) + table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false + + yield table_definition + create_sql = "CREATE TABLE #{name} (" + create_sql << table_definition.to_sql + create_sql << ") #{options[:options]}" + + execute create_sql end def drop_table(name) @@ -379,28 +385,52 @@ module ActiveRecord def add_column(table_name, column_name, type, options = {}) native_type = native_database_types[type] - add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type)}" - add_column_sql << " DEFAULT '#{options[:default]}'" if options[:default] + add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}" + add_column_options!(add_column_sql, options) execute(add_column_sql) end - + def remove_column(table_name, column_name) execute "ALTER TABLE #{table_name} DROP #{column_name}" + end + + def change_column(table_name, column_name, type, options = {}) + raise NotImplementedError, "change_column is not implemented" end def supports_migrations? false end + def rename_column(table_name, column_name, new_column_name) + raise NotImplementedError, "rename_column is not implemented" + end + + def add_index(table_name, column_name, index_type = '') + execute "CREATE #{index_type} INDEX #{table_name}_#{column_name.to_a.first}_index ON #{table_name} (#{column_name.to_a.join(", ")})" + end + + def remove_index(table_name, column_name) + execute "DROP INDEX #{table_name}_#{column_name}_index ON #{table_name}" + end + + def supports_migrations? + false + end + + def native_database_types + {} + end + + def type_to_sql(type, limit = nil) + native = native_database_types[type] + limit ||= native[:limit] + column_type_sql = native[:name] + column_type_sql << "(#{limit})" if limit + column_type_sql + end - protected - def type_to_sql(type) - native = native_database_types[type] - column_type_sql = native[:name] - column_type_sql << "(#{native[:limit]})" if native[:limit] - column_type_sql - end - + protected def log(sql, name) begin if block_given? @@ -450,19 +480,47 @@ module ActiveRecord "%s %s" % [message, dump] end end - end + + def add_column_options!(sql, options) + sql << " DEFAULT '#{options[:default]}'" if options[:default] + end + end class TableDefinition attr_accessor :columns - def initialize + def initialize(base) @columns = [] + @base = base + end + + def primary_key(name) + @columns << "#{name} #{native[:primary_key]}" + self end def column(name, type, options = {}) - @columns << [ name, type, options ] + limit = options[:limit] || native[type.to_sym][:limit] + + column_sql = "#{name} #{type_to_sql(type.to_sym, options[:limit])}" + column_sql << " DEFAULT '#{options[:default]}'" if options[:default] + @columns << column_sql self end + + def to_sql + @columns.join(", ") + end + + private + + def type_to_sql(name, limit) + @base.type_to_sql(name, limit) + end + + def native + @base.native_database_types + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index ec0558a4d5..14343b2750 100755 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -199,8 +199,19 @@ module ActiveRecord execute "CREATE DATABASE #{name}" end - def create_table(name) - super(name, "ENGINE=InnoDB") + def change_column(table_name, column_name, type, options = {}) + change_column_sql = "ALTER TABLE #{table_name} MODIFY #{column_name} #{type}" + add_column_options!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) + current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"] + execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}" + end + + def create_table(name, options = {}) + super(name, {:options => "ENGINE=InnoDB"}.merge(options)) end private diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 3d6550ea9a..16acf321da 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -60,7 +60,6 @@ module ActiveRecord # * :encoding -- An optional client encoding that is using in a SET client_encoding TO call on connection. # * :min_messages -- An optional client min messages that is using in a SET client_min_messages TO call on connection. class PostgreSQLAdapter < AbstractAdapter - def native_database_types { :primary_key => "serial primary key", @@ -132,7 +131,7 @@ module ActiveRecord %("#{name}") end - def adapter_name() + def adapter_name 'PostgreSQL' end @@ -150,8 +149,21 @@ module ActiveRecord def schema_search_path @schema_search_path ||= query('SHOW search_path')[0][0] end + + def change_column(table_name, column_name, type, options = {}) + change_column_sql = "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TYPE #{type}" + add_column_options!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) + execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}" + end - + def remove_index(table_name, column_name) + execute "DROP INDEX #{table_name}_#{column_name}_index" + end + private BYTEA_COLUMN_TYPE_OID = 17 diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index cb54d2b967..365f57f366 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -51,21 +51,32 @@ module ActiveRecord # # == Available transformations # - # * create_table(name, options = "") Creates a table called +name+ and makes the table object available to a block - # that can then add columns to it, following the same format as add_column. See example above. The options string is for + # * create_table(name, options) Creates a table called +name+ and makes the table object available to a block + # that can then add columns to it, following the same format as add_column. See example above. The options hash is for # fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition. # * drop_table(name): Drops the table called +name+. - # * add_column(table_name, column_name, type, options = {}): Adds a new column to the table called +table_name+ + # * add_column(table_name, column_name, type, options): Adds a new column to the table called +table_name+ # named +column_name+ specified to be one of the following types: # :string, :text, :integer, :float, :datetime, :timestamp, :time, :date, :binary, :boolean. A default value can be specified # by passing an +options+ hash like { :default => 11 }. + # * rename_column(table_name, column_name, new_column_name): Renames a column but keeps the type and content. + # * change_column(table_name, column_name, type, options): Changes the column to a different type using the same + # parameters as add_column. # * remove_column(table_name, column_name): Removes the column named +column_name+ from the table called +table_name+. + # * add_index(table_name, column_name): Add a new index with the name of the column on the column. + # * remove_index(table_name, column_name): Remove the index called the same as the column. # # == Irreversible transformations # # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise # an IrreversibleMigration exception in their +down+ method. # + # == Running migrations from within Rails + # + # The Rails package has support for migrations with the script/generate migration my_new_migration command and + # with the rake migrate command that'll run all the pending migrations. It'll even create the needed schema_info + # table automatically if it's missing. + # # == Database support # # Migrations are currently only supported in MySQL and PostgreSQL. diff --git a/activerecord/test/fixtures/migrations/3_innocent_jointable.rb b/activerecord/test/fixtures/migrations/3_innocent_jointable.rb new file mode 100644 index 0000000000..21c9ca5328 --- /dev/null +++ b/activerecord/test/fixtures/migrations/3_innocent_jointable.rb @@ -0,0 +1,12 @@ +class InnocentJointable < ActiveRecord::Migration + def self.up + create_table("people_reminders", :id => false) do |t| + t.column :reminder_id, :integer + t.column :person_id, :integer + end + end + + def self.down + drop_table "people_reminders" + end +end \ No newline at end of file diff --git a/activerecord/test/migration_test.rb b/activerecord/test/migration_test.rb index 72d0fbf817..a266afed2f 100644 --- a/activerecord/test/migration_test.rb +++ b/activerecord/test/migration_test.rb @@ -4,6 +4,7 @@ require File.dirname(__FILE__) + '/fixtures/migrations/1_people_have_last_names' require File.dirname(__FILE__) + '/fixtures/migrations/2_we_need_reminders' if ActiveRecord::Base.connection.supports_migrations? + class Reminder < ActiveRecord::Base; end class MigrationTest < Test::Unit::TestCase @@ -15,6 +16,7 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0" Reminder.connection.drop_table("reminders") rescue nil + Reminder.connection.drop_table("people_reminders") rescue nil Reminder.reset_column_information Person.connection.remove_column("people", "last_name") rescue nil @@ -24,11 +26,21 @@ if ActiveRecord::Base.connection.supports_migrations? Person.connection.remove_column("people", "birthday") rescue nil Person.connection.remove_column("people", "favorite_day") rescue nil Person.connection.remove_column("people", "male") rescue nil + Person.connection.remove_column("people", "administrator") rescue nil Person.reset_column_information end + + def test_add_index + Person.connection.add_column "people", "last_name", :string + + assert_nothing_raised { Person.connection.add_index("people", "last_name") } + assert_nothing_raised { Person.connection.remove_index("people", "last_name") } + + assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"]) } + assert_nothing_raised { Person.connection.remove_index("people", "last_name") } + end def test_native_types - Person.delete_all Person.connection.add_column "people", "last_name", :string Person.connection.add_column "people", "bio", :text @@ -62,21 +74,54 @@ if ActiveRecord::Base.connection.supports_migrations? Person.reset_column_information assert Person.column_methods_hash.include?(:last_name) - + PeopleHaveLastNames.down Person.reset_column_information assert !Person.column_methods_hash.include?(:last_name) end + + def test_add_rename + Person.delete_all + + Person.connection.add_column "people", "girlfriend", :string + Person.create :girlfriend => 'bobette' + + begin + Person.connection.rename_column "people", "girlfriend", "exgirlfriend" + + Person.reset_column_information + bob = Person.find(:first) + + assert_equal "bobette", bob.exgirlfriend + ensure + Person.connection.remove_column("people", "girlfriend") rescue nil + Person.connection.remove_column("people", "exgirlfriend") rescue nil + end + + end + + def test_change_column + Person.connection.add_column "people", "bio", :string + assert_nothing_raised { Person.connection.change_column "people", "bio", :text } + end + + def test_change_column_with_new_default + Person.connection.add_column "people", "administrator", :boolean, :default => 1 + assert Person.new.administrator? + + assert_nothing_raised { Person.connection.change_column "people", "administrator", :boolean, :default => 0 } + assert !Person.new.administrator? + end def test_add_table assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash } - + WeNeedReminders.up - + assert Reminder.create("content" => "hello world", "remind_at" => Time.now) assert_equal "hello world", Reminder.find(:first).content - + WeNeedReminders.down assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) } end @@ -87,7 +132,7 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/') - assert_equal 2, ActiveRecord::Migrator.current_version + assert_equal 3, ActiveRecord::Migrator.current_version Person.reset_column_information assert Person.column_methods_hash.include?(:last_name) assert Reminder.create("content" => "hello world", "remind_at" => Time.now) @@ -117,17 +162,17 @@ if ActiveRecord::Base.connection.supports_migrations? assert Reminder.create("content" => "hello world", "remind_at" => Time.now) assert_equal "hello world", Reminder.find(:first).content end - + def test_migrator_one_down ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/') - + ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 1) Person.reset_column_information assert Person.column_methods_hash.include?(:last_name) assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash } end - + def test_migrator_one_up_one_down ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 1) ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/', 0) -- cgit v1.2.3