aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG2
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb19
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/mysql_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb21
-rw-r--r--activerecord/lib/active_record/migration.rb102
-rw-r--r--activerecord/test/migration_mysql.rb104
-rw-r--r--activerecord/test/migration_test.rb139
7 files changed, 280 insertions, 112 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 1b631b2bd9..86464919de 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Added new Migrations framework for describing schema transformations in a way that can be easily applied across multiple databases #1604 [Tobias Luetke] See documentation under ActiveRecord::Migration and the additional support in the Rails rakefile/generator.
+
* Added callback hooks to association collections #1549 [Florian Weber]. Example:
class Project
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 1137ea1011..1b5c8184ae 100755
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -357,9 +357,10 @@ module ActiveRecord
sql << " OFFSET #{options[:offset]}" if options.has_key?(:offset) and !options[:offset].nil?
end
+
def initialize_schema_information
begin
- execute "CREATE TABLE schema_info (version #{native_database_types[:integer][:name]}(#{native_database_types[:integer][:limit]}))"
+ execute "CREATE TABLE schema_info (version #{type_to_sql(:integer)})"
insert "INSERT INTO schema_info (version) VALUES(0)"
rescue ActiveRecord::StatementInvalid
# Schema has been intialized
@@ -378,8 +379,7 @@ 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} #{native_type[:name]}"
- add_column_sql << "(#{options[:limit] || native_type[:limit]})" if options[:limit] || native_type[:limit]
+ add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type)}"
add_column_sql << " DEFAULT '#{options[:default]}'" if options[:default]
execute(add_column_sql)
end
@@ -387,9 +387,20 @@ module ActiveRecord
def remove_column(table_name, column_name)
execute "ALTER TABLE #{table_name} DROP #{column_name}"
end
+
+ def supports_migrations?
+ false
+ 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
+
def log(sql, name)
begin
if block_given?
@@ -439,7 +450,7 @@ module ActiveRecord
"%s %s" % [message, dump]
end
end
- end
+ end
class TableDefinition
attr_accessor :columns
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index ad0e490194..ec0558a4d5 100755
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -63,6 +63,10 @@ module ActiveRecord
"Lost connection to MySQL server during query",
"MySQL server has gone away"
]
+
+ def supports_migrations?
+ true
+ end
def native_database_types
{
@@ -89,7 +93,6 @@ module ActiveRecord
'MySQL'
end
-
def select_all(sql, name = nil)
select(sql, name)
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 66cbe3a58b..3d6550ea9a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -60,6 +60,27 @@ module ActiveRecord
# * <tt>:encoding</tt> -- An optional client encoding that is using in a SET client_encoding TO <encoding> call on connection.
# * <tt>:min_messages</tt> -- An optional client min messages that is using in a SET client_min_messages TO <min_messages> call on connection.
class PostgreSQLAdapter < AbstractAdapter
+
+ def native_database_types
+ {
+ :primary_key => "serial primary key",
+ :string => { :name => "character varying", :limit => 255 },
+ :text => { :name => "text" },
+ :integer => { :name => "integer" },
+ :float => { :name => "float" },
+ :datetime => { :name => "timestamp" },
+ :timestamp => { :name => "timestamp" },
+ :time => { :name => "timestamp" },
+ :date => { :name => "date" },
+ :binary => { :name => "bytea" },
+ :boolean => { :name => "boolean"}
+ }
+ end
+
+ def supports_migrations?
+ true
+ end
+
def select_all(sql, name = nil)
select(sql, name)
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 7a1c47038d..cb54d2b967 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -2,7 +2,102 @@ module ActiveRecord
class IrreversibleMigration < ActiveRecordError#:nodoc:
end
- class Migration #:nodoc:
+ # Migrations can manage the evolution of a schema used by several physical databases. It's a solution
+ # to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
+ # push that change to other developers and to the production server. With migrations, you can describe the transformations
+ # in self-contained classes that can be checked into version control systems and executed against another database that
+ # might be one, two, or five versions behind.
+ #
+ # Example of a simple migration:
+ #
+ # class AddSsl < ActiveRecord::Migration
+ # def self.up
+ # add_column :accounts, :ssl_enabled, :boolean, :default => 1
+ # end
+ #
+ # def self.down
+ # remove_column :accounts, :ssl_enabled
+ # end
+ # end
+ #
+ # This migration will add a boolean flag to the accounts table and remove it again, if you're backing out of the migration.
+ # It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
+ # or remove the migration. These methods can consist of both the migration specific methods, like add_column and remove_column,
+ # but may also contain regular Ruby code for generating data needed for the transformations.
+ #
+ # Example of a more complex migration that also needs to initialize data:
+ #
+ # class AddSystemSettings < ActiveRecord::Migration
+ # def self.up
+ # create_table :system_settings do |t|
+ # t.column :name, :string
+ # t.column :label, :string
+ # t.column :value, :text
+ # t.column :type, :string
+ # t.column :position, :integer
+ # end
+ #
+ # SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
+ # end
+ #
+ # def self.down
+ # drop_table :system_settings
+ # end
+ # end
+ #
+ # This migration first adds the system_settings table, then creates the very first row in it using the Active Record model
+ # that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema
+ # in one block call.
+ #
+ # == Available transformations
+ #
+ # * <tt>create_table(name, options = "")</tt> 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
+ # fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition.
+ # * <tt>drop_table(name)</tt>: Drops the table called +name+.
+ # * <tt>add_column(table_name, column_name, type, options = {})</tt>: 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 }.
+ # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
+ #
+ # == Irreversible transformations
+ #
+ # Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
+ # an <tt>IrreversibleMigration</tt> exception in their +down+ method.
+ #
+ # == Database support
+ #
+ # Migrations are currently only supported in MySQL and PostgreSQL.
+ #
+ # == More examples
+ #
+ # Not all migrations change the schema. Some just fix the data:
+ #
+ # class RemoveEmptyTags < ActiveRecord::Migration
+ # def self.up
+ # Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
+ # end
+ #
+ # def self.down
+ # # not much we can do to restore deleted data
+ # end
+ # end
+ #
+ # Others remove columns when they migrate up instead of down:
+ #
+ # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
+ # def self.up
+ # remove_column :items, :incomplete_items_count
+ # remove_column :items, :completed_items_count
+ # end
+ #
+ # def self.down
+ # add_column :items, :incomplete_items_count
+ # add_column :items, :completed_items_count
+ # end
+ # end
+ class Migration
class << self
def up() end
def down() end
@@ -17,11 +112,11 @@ module ActiveRecord
class Migrator#:nodoc:
class << self
def up(migrations_path, target_version = nil)
- new(:up, migrations_path, target_version).migrate
+ self.new(:up, migrations_path, target_version).migrate
end
def down(migrations_path, target_version = nil)
- new(:down, migrations_path, target_version).migrate
+ self.new(:down, migrations_path, target_version).migrate
end
def current_version
@@ -30,6 +125,7 @@ module ActiveRecord
end
def initialize(direction, migrations_path, target_version = nil)
+ raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
@direction, @migrations_path, @target_version = direction, migrations_path, target_version
Base.connection.initialize_schema_information
end
diff --git a/activerecord/test/migration_mysql.rb b/activerecord/test/migration_mysql.rb
deleted file mode 100644
index 6761d76ca1..0000000000
--- a/activerecord/test/migration_mysql.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-require 'abstract_unit'
-require 'fixtures/person'
-require File.dirname(__FILE__) + '/fixtures/migrations/1_people_have_last_names'
-require File.dirname(__FILE__) + '/fixtures/migrations/2_we_need_reminders'
-
-class Reminder < ActiveRecord::Base; end
-
-class MigrationTest < Test::Unit::TestCase
- def setup
- end
-
- def teardown
- ActiveRecord::Base.connection.initialize_schema_information
- ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
-
- Reminder.connection.drop_table("reminders") rescue nil
- Reminder.reset_column_information
-
- Person.connection.remove_column("people", "last_name") rescue nil
- Person.reset_column_information
- end
-
- def test_add_remove_single_field
- assert !Person.column_methods_hash.include?(:last_name)
-
- PeopleHaveLastNames.up
-
- 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_table
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash }
-
- WeNeedReminders.up
-
- assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
- assert "hello world", Reminder.find(:first)
-
- WeNeedReminders.down
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
- end
-
- def test_migrator
- assert !Person.column_methods_hash.include?(:last_name)
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash }
-
- ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
-
- assert_equal 2, 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)
- assert "hello world", Reminder.find(:first)
-
-
- ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
-
- assert_equal 0, ActiveRecord::Migrator.current_version
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
- end
-
- def test_migrator_one_up
- assert !Person.column_methods_hash.include?(:last_name)
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash }
-
- ActiveRecord::Migrator.up(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 }
-
-
- ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 2)
-
- assert Reminder.create("content" => "hello world", "remind_at" => Time.now)
- assert "hello world", Reminder.find(:first)
- 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)
-
- assert !Person.column_methods_hash.include?(:last_name)
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash }
- end
-end
diff --git a/activerecord/test/migration_test.rb b/activerecord/test/migration_test.rb
new file mode 100644
index 0000000000..72d0fbf817
--- /dev/null
+++ b/activerecord/test/migration_test.rb
@@ -0,0 +1,139 @@
+require 'abstract_unit'
+require 'fixtures/person'
+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
+ def setup
+ end
+
+ def teardown
+ ActiveRecord::Base.connection.initialize_schema_information
+ ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
+
+ Reminder.connection.drop_table("reminders") rescue nil
+ Reminder.reset_column_information
+
+ Person.connection.remove_column("people", "last_name") rescue nil
+ Person.connection.remove_column("people", "bio") rescue nil
+ Person.connection.remove_column("people", "age") rescue nil
+ Person.connection.remove_column("people", "height") rescue nil
+ 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.reset_column_information
+ end
+
+ def test_native_types
+
+ Person.delete_all
+ Person.connection.add_column "people", "last_name", :string
+ Person.connection.add_column "people", "bio", :text
+ Person.connection.add_column "people", "age", :integer
+ Person.connection.add_column "people", "height", :float
+ Person.connection.add_column "people", "birthday", :datetime
+ Person.connection.add_column "people", "favorite_day", :date
+ Person.connection.add_column "people", "male", :boolean
+ assert_nothing_raised { Person.create :first_name => 'bob', :last_name => 'bobsen', :bio => "I was born ....", :age => 18, :height => 1.78, :birthday => 18.years.ago, :favorite_day => 10.days.ago, :male => true }
+ bob = Person.find(:first)
+
+ assert_equal bob.first_name, 'bob'
+ assert_equal bob.last_name, 'bobsen'
+ assert_equal bob.bio, "I was born ...."
+ assert_equal bob.age, 18
+ assert_equal bob.male?, true
+
+ assert_equal String, bob.first_name.class
+ assert_equal String, bob.last_name.class
+ assert_equal String, bob.bio.class
+ assert_equal Fixnum, bob.age.class
+ assert_equal Time, bob.birthday.class
+ assert_equal Date, bob.favorite_day.class
+ assert_equal TrueClass, bob.male?.class
+ end
+
+ def test_add_remove_single_field
+ assert !Person.column_methods_hash.include?(:last_name)
+
+ PeopleHaveLastNames.up
+
+ 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_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
+
+ def test_migrator
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash }
+
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
+
+ assert_equal 2, 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)
+ assert_equal "hello world", Reminder.find(:first).content
+
+ ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
+
+ assert_equal 0, ActiveRecord::Migrator.current_version
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ end
+
+ def test_migrator_one_up
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash }
+
+ ActiveRecord::Migrator.up(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 }
+
+
+ ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/', 2)
+
+ 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)
+
+ assert !Person.column_methods_hash.include?(:last_name)
+ assert_raises(ActiveRecord::StatementInvalid) { Reminder.column_methods_hash }
+ end
+ end
+end \ No newline at end of file