Skip to content

Commit

Permalink
Sugared up migrations with even more bling #1609 [Tobias Luekte]
Browse files Browse the repository at this point in the history
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1697 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
dhh committed Jul 5, 2005
1 parent 2418a68 commit f1880ca
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 36 deletions.
Expand Up @@ -366,11 +366,17 @@ def initialize_schema_information
# 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)
Expand All @@ -379,28 +385,52 @@ def drop_table(name)

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?
Expand Down Expand Up @@ -450,19 +480,47 @@ def format_log_entry(message, dump = nil)
"%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
Expand Up @@ -199,8 +199,19 @@ def create_database(name)
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
Expand Down
Expand Up @@ -60,7 +60,6 @@ module ConnectionAdapters
# * <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",
Expand Down Expand Up @@ -132,7 +131,7 @@ def quote_column_name(name)
%("#{name}")
end

def adapter_name()
def adapter_name
'PostgreSQL'
end

Expand All @@ -150,8 +149,21 @@ def schema_search_path=(schema_csv)
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

Expand Down
17 changes: 14 additions & 3 deletions activerecord/lib/active_record/migration.rb
Expand Up @@ -51,21 +51,32 @@ class IrreversibleMigration < ActiveRecordError#:nodoc:
#
# == 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
# * <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 hash 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+
# * <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>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
# parameters as add_column.
# * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_name)</tt>: Add a new index with the name of the column on the column.
# * <tt>remove_index(table_name, column_name)</tt>: 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 <tt>IrreversibleMigration</tt> exception in their +down+ method.
#
# == Running migrations from within Rails
#
# The Rails package has support for migrations with the <tt>script/generate migration my_new_migration</tt> command and
# with the <tt>rake migrate</tt> 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.
Expand Down
12 changes: 12 additions & 0 deletions 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
63 changes: 54 additions & 9 deletions activerecord/test/migration_test.rb
Expand Up @@ -4,6 +4,7 @@
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
Expand All @@ -15,6 +16,7 @@ def teardown
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
Expand All @@ -24,11 +26,21 @@ def teardown
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
Expand Down Expand Up @@ -62,21 +74,54 @@ def test_add_remove_single_field

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
Expand All @@ -87,7 +132,7 @@ def test_migrator

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)
Expand Down Expand Up @@ -117,17 +162,17 @@ def test_migrator_one_up
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)
Expand Down

0 comments on commit f1880ca

Please sign in to comment.