aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2008-05-03 11:29:47 -0500
committerDavid Heinemeier Hansson <david@loudthinking.com>2008-05-03 11:29:47 -0500
commit96980bd561d79824b6cb6efbcbecdcbf8785d452 (patch)
tree66c4d506c883dbebf628c7bed020b704980d6729
parent64092de25727c1943807bf5345107d90428135a0 (diff)
downloadrails-96980bd561d79824b6cb6efbcbecdcbf8785d452.tar.gz
rails-96980bd561d79824b6cb6efbcbecdcbf8785d452.tar.bz2
rails-96980bd561d79824b6cb6efbcbecdcbf8785d452.zip
Added change_table for migrations (Jeff Dean) [#71 state:resolved]
-rw-r--r--activerecord/CHANGELOG9
-rwxr-xr-xactiverecord/README22
-rw-r--r--activerecord/RUNNING_UNIT_TESTS5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb190
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb73
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb9
-rw-r--r--activerecord/test/cases/migration_test.rb208
7 files changed, 507 insertions, 9 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 941221afc7..04cf72b38c 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,14 @@
*SVN*
+* Added change_table for migrations (Jeff Dean) [#71]. Example:
+
+ change_table :videos do |t|
+ t.timestamps # adds created_at, updated_at
+ t.belongs_to :goat # add goat_id integer
+ t.string :name, :email, :limit => 20 # adds name and email both with a 20 char limit
+ t.remove :name, :email # removes the name and email columns
+ end
+
* Fixed has_many :through .create with no parameters caused a "can't dup NilClass" error (Steven Soroka) [#85]
* Added block-setting of attributes for Base.create like Base.new already has (Adam Meehan) [#39]
diff --git a/activerecord/README b/activerecord/README
index 7204b44ec4..ff3f55ee8a 100755
--- a/activerecord/README
+++ b/activerecord/README
@@ -164,6 +164,28 @@ A short rundown of the major features:
ActiveRecord::Base.logger = Log4r::Logger.new("Application Log")
+* Database agnostic schema management with Migrations
+
+ class AddSystemSettings < ActiveRecord::Migration
+ def self.up
+ create_table :system_settings do |t|
+ t.string :name
+ t.string :label
+ t.text :value
+ t.string :type
+ t.integer :position
+ end
+
+ SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
+ end
+
+ def self.down
+ drop_table :system_settings
+ end
+ end
+
+ {Learn more}[link:classes/ActiveRecord/Migration.html]
+
== Simple example (1/2): Defining tables and classes (using MySQL)
Data definitions are specified only in the database. Active Record queries the database for
diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS
index ea7f3c02d2..39fc86759f 100644
--- a/activerecord/RUNNING_UNIT_TESTS
+++ b/activerecord/RUNNING_UNIT_TESTS
@@ -27,7 +27,10 @@ you can do so with:
rake test_mysql TEST=test/cases/base_test.rb
-That'll run the base suite using the MySQL-Ruby adapter.
+That'll run the base suite using the MySQL-Ruby adapter. Some tests rely on the schema
+being initialized - you can initialize the schema with:
+
+ rake test_mysql TEST=test/cases/aaa_create_tables_test.rb
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 7cf64f4ef8..d73ffc3da6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -469,5 +469,195 @@ module ActiveRecord
@base.native_database_types
end
end
+
+ # Represents a SQL table in an abstract way for updating a table.
+ # Also see TableDefinition and SchemaStatements#create_table
+ #
+ # Available transformations are:
+ #
+ # change_table :table do |t|
+ # t.column
+ # t.index
+ # t.timestamps
+ # t.change
+ # t.change_default
+ # t.rename
+ # t.references
+ # t.belongs_to
+ # t.string
+ # t.text
+ # t.integer
+ # t.float
+ # t.decimal
+ # t.datetime
+ # t.timestamp
+ # t.time
+ # t.date
+ # t.binary
+ # t.boolean
+ # t.remove
+ # t.remove_references
+ # t.remove_belongs_to
+ # t.remove_index
+ # t.remove_timestamps
+ # end
+ #
+ class Table
+ def initialize(table_name, base)
+ @table_name = table_name
+ @base = base
+ end
+
+ # Adds a new column to the named table.
+ # See TableDefinition#column for details of the options you can use.
+ # ===== Examples
+ # ====== Creating a simple columns
+ # t.column(:name, :string)
+ def column(column_name, type, options = {})
+ @base.add_column(@table_name, column_name, type, options)
+ end
+
+ # Adds a new index to the table. +column_name+ can be a single Symbol, or
+ # an Array of Symbols. See SchemaStatements#add_index
+ #
+ # ===== Examples
+ # ====== Creating a simple index
+ # t.index(:name)
+ # ====== Creating a unique index
+ # t.index([:branch_id, :party_id], :unique => true)
+ # ====== Creating a named index
+ # t.index([:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
+ def index(column_name, options = {})
+ @base.add_index(@table_name, column_name, options)
+ end
+
+ # Adds timestamps (created_at and updated_at) columns to the table. See SchemaStatements#timestamps
+ # ===== Examples
+ # t.timestamps
+ def timestamps
+ @base.add_timestamps(@table_name)
+ end
+
+ # Changes the column's definition according to the new options.
+ # See TableDefinition#column for details of the options you can use.
+ # ===== Examples
+ # t.change(:name, :string, :limit => 80)
+ # t.change(:description, :text)
+ def change(column_name, type, options = {})
+ @base.change_column(@table_name, column_name, type, options)
+ end
+
+ # Sets a new default value for a column. See
+ # ===== Examples
+ # t.change_default(:qualification, 'new')
+ # t.change_default(:authorized, 1)
+ def change_default(column_name, default)
+ @base.change_column_default(@table_name, column_name, default)
+ end
+
+ # Removes the column(s) from the table definition.
+ # ===== Examples
+ # t.remove(:qualification)
+ # t.remove(:qualification, :experience)
+ # t.removes(:qualification, :experience)
+ def remove(*column_names)
+ @base.remove_column(@table_name, column_names)
+ end
+
+ # Remove the given index from the table.
+ #
+ # Remove the suppliers_name_index in the suppliers table.
+ # t.remove_index :name
+ # Remove the index named accounts_branch_id_index in the accounts table.
+ # t.remove_index :column => :branch_id
+ # Remove the index named accounts_branch_id_party_id_index in the accounts table.
+ # t.remove_index :column => [:branch_id, :party_id]
+ # Remove the index named by_branch_party in the accounts table.
+ # t.remove_index :name => :by_branch_party
+ def remove_index(options = {})
+ @base.remove_index(@table_name, options)
+ end
+
+ # Removes the timestamp columns (created_at and updated_at) from the table.
+ # ===== Examples
+ # t.remove_timestamps
+ def remove_timestamps
+ @base.remove_timestamps(@table_name)
+ end
+
+ # Renames a column.
+ # ===== Example
+ # t.rename(:description, :name)
+ def rename(column_name, new_column_name)
+ @base.rename_column(@table_name, column_name, new_column_name)
+ end
+
+ # Adds a reference. Optionally adds a +type+ column. <tt>reference</tt>,
+ # <tt>references</tt> and <tt>belongs_to</tt> are all acceptable
+ # ===== Example
+ # t.references(:goat)
+ # t.references(:goat, :polymorphic => true)
+ # t.references(:goat)
+ # t.belongs_to(:goat)
+ def references(*args)
+ options = args.extract_options!
+ polymorphic = options.delete(:polymorphic)
+ args.each do |col|
+ @base.add_column(@table_name, "#{col}_id", :integer, options)
+ @base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
+ end
+ end
+ alias :belongs_to :references
+
+ # Adds a reference. Optionally removes a +type+ column. <tt>remove_reference</tt>,
+ # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are all acceptable
+ # ===== Example
+ # t.remove_reference(:goat)
+ # t.remove_reference(:goat, :polymorphic => true)
+ # t.remove_references(:goat)
+ # t.remove_belongs_to(:goat)
+ def remove_references(*args)
+ options = args.extract_options!
+ polymorphic = options.delete(:polymorphic)
+ args.each do |col|
+ @base.remove_column(@table_name, "#{col}_id")
+ @base.remove_column(@table_name, "#{col}_type") unless polymorphic.nil?
+ end
+ end
+ alias :remove_belongs_to :remove_references
+
+ # Adds a column or columns of a specified type
+ # ===== Example
+ # t.string(:goat)
+ # t.string(:goat, :sheep)
+ %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
+ class_eval <<-EOV
+ def #{column_type}(*args)
+ options = args.extract_options!
+ column_names = args
+
+ column_names.each do |name|
+ column = ColumnDefinition.new(@base, name, '#{column_type}')
+ if options[:limit]
+ column.limit = options[:limit]
+ elsif native['#{column_type}'.to_sym].is_a?(Hash)
+ column.limit = native['#{column_type}'.to_sym][:limit]
+ end
+ column.precision = options[:precision]
+ column.scale = options[:scale]
+ column.default = options[:default]
+ column.null = options[:null]
+ @base.add_column(@table_name, name, column.sql_type, options)
+ end
+ end
+ EOV
+ end
+
+ private
+ def native
+ @base.native_database_types
+ end
+ end
+
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 64a7e9fabe..6aae556d67 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -104,6 +104,67 @@ module ActiveRecord
execute create_sql
end
+ # A block for changing columns in +table+.
+ #
+ # === Example
+ # # change_table() yields a Table instance
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, :limit => 60
+ # # Other column alterations here
+ # end
+ #
+ # ===== Examples
+ # ====== Add a column
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, :limit => 60
+ # end
+ #
+ # ====== Add 2 integer columns
+ # change_table(:suppliers) do |t|
+ # t.integer :width, :height, :null => false, :default => 0
+ # end
+ #
+ # ====== Add created_at/updated_at columns
+ # change_table(:suppliers) do |t|
+ # t.timestamps
+ # end
+ #
+ # ====== Add a foreign key column
+ # change_table(:suppliers) do |t|
+ # t.references :company
+ # end
+ #
+ # Creates a <tt>company_id(integer)</tt> column
+ #
+ # ====== Add a polymorphic foreign key column
+ # change_table(:suppliers) do |t|
+ # t.belongs_to :company, :polymorphic => true
+ # end
+ #
+ # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns
+ #
+ # ====== Remove a column
+ # change_table(:suppliers) do |t|
+ # t.remove :company
+ # end
+ #
+ # ====== Remove a column
+ # change_table(:suppliers) do |t|
+ # t.remove :company_id
+ # t.remove :width, :height
+ # end
+ #
+ # ====== Remove an index
+ # change_table(:suppliers) do |t|
+ # t.remove_index :company_id
+ # end
+ #
+ # See also Table for details on
+ # all of the various column transformation
+ def change_table(table_name)
+ yield Table.new(table_name, self)
+ end
+
# Renames a table.
# ===== Example
# rename_table('octopuses', 'octopi')
@@ -124,13 +185,17 @@ module ActiveRecord
execute(add_column_sql)
end
- # Removes the column from the table definition.
+ # Removes the column(s) from the table definition.
# ===== Examples
# remove_column(:suppliers, :qualification)
- def remove_column(table_name, column_name)
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
+ # remove_columns(:suppliers, :qualification, :experience)
+ def remove_column(table_name, *column_names)
+ column_names.flatten.each do |column_name|
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
+ end
end
-
+ alias :remove_columns :remove_column
+
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
# ===== Examples
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 59a51c0279..8fa62c1845 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -219,11 +219,14 @@ module ActiveRecord
execute "VACUUM"
end
- def remove_column(table_name, column_name) #:nodoc:
- alter_table(table_name) do |definition|
- definition.columns.delete(definition[column_name])
+ def remove_column(table_name, *column_names) #:nodoc:
+ column_names.flatten.each do |column_name|
+ alter_table(table_name) do |definition|
+ definition.columns.delete(definition[column_name])
+ end
end
end
+ alias :remove_columns :remove_column
def change_column_default(table_name, column_name, default) #:nodoc:
alter_table(table_name) do |definition|
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index f99e736c55..d4e81827aa 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1077,7 +1077,7 @@ if ActiveRecord::Base.connection.supports_migrations?
protected
def with_new_table
- Person.connection.create_table :delete_me do |t|
+ Person.connection.create_table :delete_me, :force => true do |t|
yield t
end
ensure
@@ -1086,4 +1086,210 @@ if ActiveRecord::Base.connection.supports_migrations?
end # SexyMigrationsTest
end # uses_mocha
+
+ uses_mocha 'ChangeTable migration tests' do
+ class ChangeTableMigrationsTest < ActiveRecord::TestCase
+ def setup
+ @connection = Person.connection
+ @connection.create_table :delete_me, :force => true do |t|
+ end
+ end
+
+ def teardown
+ Person.connection.drop_table :delete_me rescue nil
+ end
+
+ def test_references_column_type_adds_id
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
+ t.references :customer
+ end
+ end
+
+ def test_remove_references_column_type_removes_id
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'customer_id')
+ t.remove_references :customer
+ end
+ end
+
+ def test_add_belongs_to_works_like_add_references
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
+ t.belongs_to :customer
+ end
+ end
+
+ def test_remove_belongs_to_works_like_remove_references
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'customer_id')
+ t.remove_belongs_to :customer
+ end
+ end
+
+ def test_references_column_type_with_polymorphic_adds_type
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {})
+ @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {})
+ t.references :taggable, :polymorphic => true
+ end
+ end
+
+ def test_remove_references_column_type_with_polymorphic_removes_type
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
+ t.remove_references :taggable, :polymorphic => true
+ end
+ end
+
+ def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false})
+ @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false})
+ t.references :taggable, :polymorphic => true, :null => false
+ end
+ end
+
+ def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
+ t.remove_references :taggable, :polymorphic => true, :null => false
+ end
+ end
+
+ def test_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expects(:add_timestamps).with(:delete_me)
+ t.timestamps
+ end
+ end
+
+ def test_remove_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expects(:remove_timestamps).with(:delete_me)
+ t.remove_timestamps
+ end
+ end
+
+ def string_column
+ if current_adapter?(:PostgreSQLAdapter)
+ "character varying(255)"
+ else
+ 'varchar(255)'
+ end
+ end
+
+ def integer_column
+ if current_adapter?(:SQLite3Adapter) || current_adapter?(:SQLiteAdapter) || current_adapter?(:PostgreSQLAdapter)
+ "integer"
+ else
+ 'int(11)'
+ end
+ end
+
+ def test_integer_creates_integer_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {})
+ @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {})
+ t.integer :foo, :bar
+ end
+ end
+
+ def test_string_creates_string_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :foo, string_column, {})
+ @connection.expects(:add_column).with(:delete_me, :bar, string_column, {})
+ t.string :foo, :bar
+ end
+ end
+
+ def test_column_creates_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :bar, :integer, {})
+ t.column :bar, :integer
+ end
+ end
+
+ def test_column_creates_column_with_options
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false})
+ t.column :bar, :integer, :null => false
+ end
+ end
+
+ def test_index_creates_index
+ with_change_table do |t|
+ @connection.expects(:add_index).with(:delete_me, :bar, {})
+ t.index :bar
+ end
+ end
+
+ def test_index_creates_index_with_options
+ with_change_table do |t|
+ @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true})
+ t.index :bar, :unique => true
+ end
+ end
+
+ def test_change_changes_column
+ with_change_table do |t|
+ @connection.expects(:change_column).with(:delete_me, :bar, :string, {})
+ t.change :bar, :string
+ end
+ end
+
+ def test_change_changes_column_with_options
+ with_change_table do |t|
+ @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true})
+ t.change :bar, :string, :null => true
+ end
+ end
+
+ def test_change_default_changes_column
+ with_change_table do |t|
+ @connection.expects(:change_column_default).with(:delete_me, :bar, :string)
+ t.change_default :bar, :string
+ end
+ end
+
+ def test_remove_drops_single_column
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, [:bar])
+ t.remove :bar
+ end
+ end
+
+ def test_remove_drops_multiple_columns
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, [:bar, :baz])
+ t.remove :bar, :baz
+ end
+ end
+
+ def test_remove_index_removes_index_with_options
+ with_change_table do |t|
+ @connection.expects(:remove_index).with(:delete_me, {:unique => true})
+ t.remove_index :unique => true
+ end
+ end
+
+ def test_rename_renames_column
+ with_change_table do |t|
+ @connection.expects(:rename_column).with(:delete_me, :bar, :baz)
+ t.rename :bar, :baz
+ end
+ end
+
+ protected
+ def with_change_table
+ Person.connection.change_table :delete_me do |t|
+ yield t
+ end
+ end
+
+ end # ChangeTable test
+ end # uses_mocha
+
end