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
This commit is contained in:
parent
2418a68aa2
commit
f1880cac58
@ -366,11 +366,17 @@ def initialize_schema_information
|
||||
# Schema has been intialized
|
||||
end
|
||||
end
|
||||
|
||||
def create_table(name, options = {})
|
||||
table_definition = TableDefinition.new(self)
|
||||
table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
|
||||
|
||||
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) }
|
||||
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 @@ 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
|
||||
|
||||
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 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 log(sql, name)
|
||||
begin
|
||||
if block_given?
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
@ -132,7 +131,7 @@ def quote_column_name(name)
|
||||
%("#{name}")
|
||||
end
|
||||
|
||||
def adapter_name()
|
||||
def adapter_name
|
||||
'PostgreSQL'
|
||||
end
|
||||
|
||||
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
12
activerecord/test/fixtures/migrations/3_innocent_jointable.rb
vendored
Normal file
12
activerecord/test/fixtures/migrations/3_innocent_jointable.rb
vendored
Normal file
@ -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
|
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user