From 96980bd561d79824b6cb6efbcbecdcbf8785d452 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 3 May 2008 11:29:47 -0500 Subject: Added change_table for migrations (Jeff Dean) [#71 state:resolved] --- activerecord/CHANGELOG | 9 + activerecord/README | 22 +++ activerecord/RUNNING_UNIT_TESTS | 5 +- .../abstract/schema_definitions.rb | 190 +++++++++++++++++++ .../abstract/schema_statements.rb | 73 +++++++- .../connection_adapters/sqlite_adapter.rb | 9 +- activerecord/test/cases/migration_test.rb | 208 ++++++++++++++++++++- 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. reference, + # references and belongs_to 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. remove_reference, + # remove_references and remove_belongs_to 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 company_id(integer) column + # + # ====== Add a polymorphic foreign key column + # change_table(:suppliers) do |t| + # t.belongs_to :company, :polymorphic => true + # end + # + # Creates company_type(varchar) and company_id(integer) 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 -- cgit v1.2.3