diff options
Diffstat (limited to 'activerecord/test/cases/migration')
17 files changed, 3553 insertions, 0 deletions
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb new file mode 100644 index 0000000000..1494027182 --- /dev/null +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -0,0 +1,467 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class ChangeSchemaTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + teardown do + connection.drop_table :testings rescue nil + ActiveRecord::Base.primary_key_prefix_type = nil + ActiveRecord::Base.clear_cache! + end + + def test_create_table_without_id + testing_table_with_only_foo_attribute do + assert_equal connection.columns(:testings).size, 1 + end + end + + def test_add_column_with_primary_key_attribute + testing_table_with_only_foo_attribute do + connection.add_column :testings, :id, :primary_key + assert_equal connection.columns(:testings).size, 2 + end + end + + def test_create_table_adds_id + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert_equal %w(id foo), connection.columns(:testings).map(&:name) + end + + def test_create_table_with_not_null_column + connection.create_table :testings do |t| + t.column :foo, :string, null: false + end + + assert_raises(ActiveRecord::NotNullViolation) do + connection.execute "insert into testings (foo) values (NULL)" + end + end + + def test_create_table_with_defaults + # MySQL doesn't allow defaults on TEXT or BLOB columns. + mysql = current_adapter?(:Mysql2Adapter) + + connection.create_table :testings do |t| + t.column :one, :string, default: "hello" + t.column :two, :boolean, default: true + t.column :three, :boolean, default: false + t.column :four, :integer, default: 1 + t.column :five, :text, default: "hello" unless mysql + end + + columns = connection.columns(:testings) + one = columns.detect { |c| c.name == "one" } + two = columns.detect { |c| c.name == "two" } + three = columns.detect { |c| c.name == "three" } + four = columns.detect { |c| c.name == "four" } + five = columns.detect { |c| c.name == "five" } unless mysql + + assert_equal "hello", one.default + assert_equal true, connection.lookup_cast_type_from_column(two).deserialize(two.default) + assert_equal false, connection.lookup_cast_type_from_column(three).deserialize(three.default) + assert_equal "1", four.default + assert_equal "hello", five.default unless mysql + end + + if current_adapter?(:PostgreSQLAdapter) + def test_add_column_with_array + connection.create_table :testings + connection.add_column :testings, :foo, :string, array: true + + columns = connection.columns(:testings) + array_column = columns.detect { |c| c.name == "foo" } + + assert_predicate array_column, :array? + end + + def test_create_table_with_array_column + connection.create_table :testings do |t| + t.string :foo, array: true + end + + columns = connection.columns(:testings) + array_column = columns.detect { |c| c.name == "foo" } + + assert_predicate array_column, :array? + end + end + + def test_create_table_with_bigint + connection.create_table :testings do |t| + t.bigint :eight_int + end + columns = connection.columns(:testings) + eight = columns.detect { |c| c.name == "eight_int" } + + if current_adapter?(:OracleAdapter) + assert_equal "NUMBER(19)", eight.sql_type + elsif current_adapter?(:SQLite3Adapter) + assert_equal "bigint", eight.sql_type + else + assert_equal :integer, eight.type + assert_equal 8, eight.limit + end + ensure + connection.drop_table :testings + end + + def test_create_table_with_limits + connection.create_table :testings do |t| + t.column :foo, :string, limit: 255 + + t.column :default_int, :integer + + t.column :one_int, :integer, limit: 1 + t.column :four_int, :integer, limit: 4 + t.column :eight_int, :integer, limit: 8 + end + + columns = connection.columns(:testings) + foo = columns.detect { |c| c.name == "foo" } + assert_equal 255, foo.limit + + default = columns.detect { |c| c.name == "default_int" } + one = columns.detect { |c| c.name == "one_int" } + four = columns.detect { |c| c.name == "four_int" } + eight = columns.detect { |c| c.name == "eight_int" } + + if current_adapter?(:PostgreSQLAdapter) + assert_equal "integer", default.sql_type + assert_equal "smallint", one.sql_type + assert_equal "integer", four.sql_type + assert_equal "bigint", eight.sql_type + elsif current_adapter?(:Mysql2Adapter) + assert_match "int(11)", default.sql_type + assert_match "tinyint", one.sql_type + assert_match "int", four.sql_type + assert_match "bigint", eight.sql_type + elsif current_adapter?(:OracleAdapter) + assert_equal "NUMBER(38)", default.sql_type + assert_equal "NUMBER(1)", one.sql_type + assert_equal "NUMBER(4)", four.sql_type + assert_equal "NUMBER(8)", eight.sql_type + end + end + + def test_create_table_with_primary_key_prefix_as_table_name_with_underscore + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert_equal %w(testing_id foo), connection.columns(:testings).map(&:name) + end + + def test_create_table_with_primary_key_prefix_as_table_name + ActiveRecord::Base.primary_key_prefix_type = :table_name + + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert_equal %w(testingid foo), connection.columns(:testings).map(&:name) + end + + def test_create_table_raises_when_redefining_primary_key_column + error = assert_raise(ArgumentError) do + connection.create_table :testings do |t| + t.column :id, :string + end + end + + assert_equal "you can't redefine the primary key column 'id'. To define a custom primary key, pass { id: false } to create_table.", error.message + end + + def test_create_table_raises_when_redefining_custom_primary_key_column + error = assert_raise(ArgumentError) do + connection.create_table :testings, primary_key: :testing_id do |t| + t.column :testing_id, :string + end + end + + assert_equal "you can't redefine the primary key column 'testing_id'. To define a custom primary key, pass { id: false } to create_table.", error.message + end + + def test_create_table_with_timestamps_should_create_datetime_columns + connection.create_table table_name do |t| + t.timestamps + end + created_columns = connection.columns(table_name) + + created_at_column = created_columns.detect { |c| c.name == "created_at" } + updated_at_column = created_columns.detect { |c| c.name == "updated_at" } + + assert !created_at_column.null + assert !updated_at_column.null + end + + def test_create_table_with_timestamps_should_create_datetime_columns_with_options + connection.create_table table_name do |t| + t.timestamps null: true + end + created_columns = connection.columns(table_name) + + created_at_column = created_columns.detect { |c| c.name == "created_at" } + updated_at_column = created_columns.detect { |c| c.name == "updated_at" } + + assert created_at_column.null + assert updated_at_column.null + end + + def test_create_table_without_a_block + connection.create_table table_name + end + + # SQLite3 will not allow you to add a NOT NULL + # column to a table without a default value. + unless current_adapter?(:SQLite3Adapter) + def test_add_column_not_null_without_default + connection.create_table :testings do |t| + t.column :foo, :string + end + connection.add_column :testings, :bar, :string, null: false + + assert_raise(ActiveRecord::NotNullViolation) do + connection.execute "insert into testings (foo, bar) values ('hello', NULL)" + end + end + end + + def test_add_column_not_null_with_default + connection.create_table :testings do |t| + t.column :foo, :string + end + + quoted_id = connection.quote_column_name("id") + quoted_foo = connection.quote_column_name("foo") + quoted_bar = connection.quote_column_name("bar") + connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}) values (1, 'hello')") + assert_nothing_raised do + connection.add_column :testings, :bar, :string, null: false, default: "default" + end + + assert_raises(ActiveRecord::NotNullViolation) do + connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}, #{quoted_bar}) values (2, 'hello', NULL)") + end + end + + def test_add_column_with_timestamp_type + connection.create_table :testings do |t| + t.column :foo, :timestamp + end + + column = connection.columns(:testings).find { |c| c.name == "foo" } + + assert_equal :datetime, column.type + + if current_adapter?(:PostgreSQLAdapter) + assert_equal "timestamp without time zone", column.sql_type + elsif current_adapter?(:Mysql2Adapter) + assert_equal "timestamp", column.sql_type + elsif current_adapter?(:OracleAdapter) + assert_equal "TIMESTAMP(6)", column.sql_type + else + assert_equal connection.type_to_sql("datetime"), column.sql_type + end + end + + def test_change_column_quotes_column_names + connection.create_table :testings do |t| + t.column :select, :string + end + + connection.change_column :testings, :select, :string, limit: 10 + + # Oracle needs primary key value from sequence + if current_adapter?(:OracleAdapter) + connection.execute "insert into testings (id, #{connection.quote_column_name('select')}) values (testings_seq.nextval, '7 chars')" + else + connection.execute "insert into testings (#{connection.quote_column_name('select')}) values ('7 chars')" + end + end + + def test_keeping_default_and_notnull_constraints_on_change + connection.create_table :testings do |t| + t.column :title, :string + end + person_klass = Class.new(ActiveRecord::Base) + person_klass.table_name = "testings" + + person_klass.connection.add_column "testings", "wealth", :integer, null: false, default: 99 + person_klass.reset_column_information + assert_equal 99, person_klass.column_defaults["wealth"] + assert_equal false, person_klass.columns_hash["wealth"].null + # Oracle needs primary key value from sequence + if current_adapter?(:OracleAdapter) + assert_nothing_raised { person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')") } + else + assert_nothing_raised { person_klass.connection.execute("insert into testings (title) values ('tester')") } + end + + # change column default to see that column doesn't lose its not null definition + person_klass.connection.change_column_default "testings", "wealth", 100 + person_klass.reset_column_information + assert_equal 100, person_klass.column_defaults["wealth"] + assert_equal false, person_klass.columns_hash["wealth"].null + + # rename column to see that column doesn't lose its not null and/or default definition + person_klass.connection.rename_column "testings", "wealth", "money" + person_klass.reset_column_information + assert_nil person_klass.columns_hash["wealth"] + assert_equal 100, person_klass.column_defaults["money"] + assert_equal false, person_klass.columns_hash["money"].null + + # change column + person_klass.connection.change_column "testings", "money", :integer, null: false, default: 1000 + person_klass.reset_column_information + assert_equal 1000, person_klass.column_defaults["money"] + assert_equal false, person_klass.columns_hash["money"].null + + # change column, make it nullable and clear default + person_klass.connection.change_column "testings", "money", :integer, null: true, default: nil + person_klass.reset_column_information + assert_nil person_klass.columns_hash["money"].default + assert_equal true, person_klass.columns_hash["money"].null + + # change_column_null, make it not nullable and set null values to a default value + person_klass.connection.execute("UPDATE testings SET money = NULL") + person_klass.connection.change_column_null "testings", "money", false, 2000 + person_klass.reset_column_information + assert_nil person_klass.columns_hash["money"].default + assert_equal false, person_klass.columns_hash["money"].null + assert_equal 2000, connection.select_values("SELECT money FROM testings").first.to_i + end + + def test_change_column_null + testing_table_with_only_foo_attribute do + notnull_migration = Class.new(ActiveRecord::Migration::Current) do + def change + change_column_null :testings, :foo, false + end + end + notnull_migration.new.suppress_messages do + notnull_migration.migrate(:up) + assert_equal false, connection.columns(:testings).find { |c| c.name == "foo" }.null + notnull_migration.migrate(:down) + assert connection.columns(:testings).find { |c| c.name == "foo" }.null + end + end + end + + def test_column_exists + connection.create_table :testings do |t| + t.column :foo, :string + end + + assert connection.column_exists?(:testings, :foo) + assert_not connection.column_exists?(:testings, :bar) + end + + def test_column_exists_with_type + connection.create_table :testings do |t| + t.column :foo, :string + t.column :bar, :decimal, precision: 8, scale: 2 + end + + assert connection.column_exists?(:testings, :foo, :string) + assert_not connection.column_exists?(:testings, :foo, :integer) + + assert connection.column_exists?(:testings, :bar, :decimal) + assert_not connection.column_exists?(:testings, :bar, :integer) + end + + def test_column_exists_with_definition + connection.create_table :testings do |t| + t.column :foo, :string, limit: 100 + t.column :bar, :decimal, precision: 8, scale: 2 + t.column :taggable_id, :integer, null: false + t.column :taggable_type, :string, default: "Photo" + end + + assert connection.column_exists?(:testings, :foo, :string, limit: 100) + assert_not connection.column_exists?(:testings, :foo, :string, limit: nil) + assert connection.column_exists?(:testings, :bar, :decimal, precision: 8, scale: 2) + assert_not connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil) + assert connection.column_exists?(:testings, :taggable_id, :integer, null: false) + assert_not connection.column_exists?(:testings, :taggable_id, :integer, null: true) + assert connection.column_exists?(:testings, :taggable_type, :string, default: "Photo") + assert_not connection.column_exists?(:testings, :taggable_type, :string, default: nil) + end + + def test_column_exists_on_table_with_no_options_parameter_supplied + connection.create_table :testings do |t| + t.string :foo + end + connection.change_table :testings do |t| + assert t.column_exists?(:foo) + assert !(t.column_exists?(:bar)) + end + end + + def test_drop_table_if_exists + connection.create_table(:testings) + assert connection.table_exists?(:testings) + connection.drop_table(:testings, if_exists: true) + assert_not connection.table_exists?(:testings) + end + + def test_drop_table_if_exists_nothing_raised + assert_nothing_raised { connection.drop_table(:nonexistent, if_exists: true) } + end + + private + def testing_table_with_only_foo_attribute + connection.create_table :testings, id: false do |t| + t.column :foo, :string + end + + yield + end + end + + if ActiveRecord::Base.connection.supports_foreign_keys? + class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :trains + @connection.create_table(:wagons) { |t| t.references :train } + @connection.add_foreign_key :wagons, :trains + end + + teardown do + [:wagons, :trains].each do |table| + @connection.drop_table table, if_exists: true + end + end + + def test_create_table_with_force_cascade_drops_dependent_objects + skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter) + # can't re-create table referenced by foreign key + assert_raises(ActiveRecord::StatementInvalid) do + @connection.create_table :trains, force: true + end + + # can recreate referenced table with force: :cascade + @connection.create_table :trains, force: :cascade + assert_equal [], @connection.foreign_keys(:wagons) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb new file mode 100644 index 0000000000..034bf32165 --- /dev/null +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -0,0 +1,258 @@ +# frozen_string_literal: true + +require "cases/migration/helper" + +module ActiveRecord + class Migration + class TableTest < ActiveRecord::TestCase + def setup + @connection = Minitest::Mock.new + end + + teardown do + assert @connection.verify + end + + def with_change_table + yield ActiveRecord::Base.connection.update_table_definition(:delete_me, @connection) + end + + def test_references_column_type_adds_id + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :customer, {}] + t.references :customer + end + end + + def test_remove_references_column_type_removes_id + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :customer, {}] + t.remove_references :customer + end + end + + def test_add_belongs_to_works_like_add_references + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :customer, {}] + t.belongs_to :customer + end + end + + def test_remove_belongs_to_works_like_remove_references + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :customer, {}] + t.remove_belongs_to :customer + end + end + + def test_references_column_type_with_polymorphic_adds_type + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true] + t.references :taggable, polymorphic: true + end + end + + def test_remove_references_column_type_with_polymorphic_removes_type + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true] + 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.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, 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.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, null: false] + t.remove_references :taggable, polymorphic: true, null: false + end + end + + def test_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.references :taggable, polymorphic: true, type: :string + end + end + + def test_remove_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.remove_references :taggable, polymorphic: true, type: :string + end + end + + def test_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :add_timestamps, nil, [:delete_me, null: true] + t.timestamps null: true + end + end + + def test_remove_timestamps_creates_updated_at_and_created_at + with_change_table do |t| + @connection.expect :remove_timestamps, nil, [:delete_me, { null: true }] + t.remove_timestamps(null: true) + end + end + + def test_primary_key_creates_primary_key_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true] + t.primary_key :id, first: true + end + end + + def test_integer_creates_integer_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :integer, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] + t.integer :foo, :bar + end + end + + def test_bigint_creates_bigint_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :bigint, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :bigint, {}] + t.bigint :foo, :bar + end + end + + def test_string_creates_string_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :string, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :string, {}] + t.string :foo, :bar + end + end + + if current_adapter?(:PostgreSQLAdapter) + def test_json_creates_json_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :json, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :json, {}] + t.json :foo, :bar + end + end + + def test_xml_creates_xml_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :foo, :xml, {}] + @connection.expect :add_column, nil, [:delete_me, :bar, :xml, {}] + t.xml :foo, :bar + end + end + end + + def test_column_creates_column + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] + t.column :bar, :integer + end + end + + def test_column_creates_column_with_options + with_change_table do |t| + @connection.expect :add_column, nil, [: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.expect :add_index, nil, [:delete_me, :bar, {}] + t.index :bar + end + end + + def test_index_creates_index_with_options + with_change_table do |t| + @connection.expect :add_index, nil, [:delete_me, :bar, { unique: true }] + t.index :bar, unique: true + end + end + + def test_index_exists + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, {}] + t.index_exists?(:bar) + end + end + + def test_index_exists_with_options + with_change_table do |t| + @connection.expect :index_exists?, nil, [:delete_me, :bar, { unique: true }] + t.index_exists?(:bar, unique: true) + end + end + + def test_rename_index_renames_index + with_change_table do |t| + @connection.expect :rename_index, nil, [:delete_me, :bar, :baz] + t.rename_index :bar, :baz + end + end + + def test_change_changes_column + with_change_table do |t| + @connection.expect :change_column, nil, [:delete_me, :bar, :string, {}] + t.change :bar, :string + end + end + + def test_change_changes_column_with_options + with_change_table do |t| + @connection.expect :change_column, nil, [: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.expect :change_column_default, nil, [:delete_me, :bar, :string] + t.change_default :bar, :string + end + end + + def test_remove_drops_single_column + with_change_table do |t| + @connection.expect :remove_columns, nil, [:delete_me, :bar] + t.remove :bar + end + end + + def test_remove_drops_multiple_columns + with_change_table do |t| + @connection.expect :remove_columns, nil, [:delete_me, :bar, :baz] + t.remove :bar, :baz + end + end + + def test_remove_index_removes_index_with_options + with_change_table do |t| + @connection.expect :remove_index, nil, [:delete_me, { unique: true }] + t.remove_index unique: true + end + end + + def test_rename_renames_column + with_change_table do |t| + @connection.expect :rename_column, nil, [:delete_me, :bar, :baz] + t.rename :bar, :baz + end + end + + def test_table_name_set + with_change_table do |t| + assert_equal :delete_me, t.name + end + end + end + end +end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb new file mode 100644 index 0000000000..3022121f4c --- /dev/null +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require "cases/migration/helper" + +module ActiveRecord + class Migration + class ColumnAttributesTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_tests = false + + def test_add_column_newline_default + string = "foo\nbar" + add_column "test_models", "command", :string, default: string + TestModel.reset_column_information + + assert_equal string, TestModel.new.command + end + + def test_add_remove_single_field_using_string_arguments + assert_no_column TestModel, :last_name + + add_column "test_models", "last_name", :string + assert_column TestModel, :last_name + + remove_column "test_models", "last_name" + assert_no_column TestModel, :last_name + end + + def test_add_remove_single_field_using_symbol_arguments + assert_no_column TestModel, :last_name + + add_column :test_models, :last_name, :string + assert_column TestModel, :last_name + + remove_column :test_models, :last_name + assert_no_column TestModel, :last_name + end + + def test_add_column_without_limit + # TODO: limit: nil should work with all adapters. + skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:Mysql2Adapter) + add_column :test_models, :description, :string, limit: nil + TestModel.reset_column_information + assert_nil TestModel.columns_hash["description"].limit + end + + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + def test_unabstracted_database_dependent_types + add_column :test_models, :intelligence_quotient, :smallint + TestModel.reset_column_information + assert_match(/smallint/, TestModel.columns_hash["intelligence_quotient"].sql_type) + end + end + + unless current_adapter?(:SQLite3Adapter) + # We specifically do a manual INSERT here, and then test only the SELECT + # functionality. This allows us to more easily catch INSERT being broken, + # but SELECT actually working fine. + def test_native_decimal_insert_manual_vs_automatic + correct_value = "0012345678901234567890.0123456789".to_d + + connection.add_column "test_models", "wealth", :decimal, precision: "30", scale: "10" + + # Do a manual insertion + if current_adapter?(:OracleAdapter) + connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" + else + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + end + + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth + + # If this assert fails, that means the SELECT is broken! + assert_equal correct_value, row.wealth + + # Reset to old state + TestModel.delete_all + + # Now use the Rails insertion + TestModel.create wealth: BigDecimal("12345678901234567890.0123456789") + + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth + + # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! + assert_equal correct_value, row.wealth + end + end + + def test_add_column_with_precision_and_scale + connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7 + + wealth_column = TestModel.columns_hash["wealth"] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + end + + # Test SQLite3 adapter specifically for decimal types with precision and scale + # attributes, since these need to be maintained in schema but aren't actually + # used in SQLite3 itself + if current_adapter?(:SQLite3Adapter) + def test_change_column_with_new_precision_and_scale + connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7 + + connection.change_column "test_models", "wealth", :decimal, precision: 12, scale: 8 + TestModel.reset_column_information + + wealth_column = TestModel.columns_hash["wealth"] + assert_equal 12, wealth_column.precision + assert_equal 8, wealth_column.scale + end + + def test_change_column_preserve_other_column_precision_and_scale + connection.add_column "test_models", "last_name", :string + connection.add_column "test_models", "wealth", :decimal, precision: 9, scale: 7 + + wealth_column = TestModel.columns_hash["wealth"] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + + connection.change_column "test_models", "last_name", :string, null: false + TestModel.reset_column_information + + wealth_column = TestModel.columns_hash["wealth"] + assert_equal 9, wealth_column.precision + assert_equal 7, wealth_column.scale + end + end + + unless current_adapter?(:SQLite3Adapter) + def test_native_types + add_column "test_models", "first_name", :string + add_column "test_models", "last_name", :string + add_column "test_models", "bio", :text + add_column "test_models", "age", :integer + add_column "test_models", "height", :float + add_column "test_models", "wealth", :decimal, precision: "30", scale: "10" + add_column "test_models", "birthday", :datetime + add_column "test_models", "favorite_day", :date + add_column "test_models", "moment_of_truth", :datetime + add_column "test_models", "male", :boolean + + TestModel.create first_name: "bob", last_name: "bobsen", + bio: "I was born ....", age: 18, height: 1.78, + wealth: BigDecimal("12345678901234567890.0123456789"), + birthday: 18.years.ago, favorite_day: 10.days.ago, + moment_of_truth: "1782-10-10 21:40:18", male: true + + bob = TestModel.first + assert_equal "bob", bob.first_name + assert_equal "bobsen", bob.last_name + assert_equal "I was born ....", bob.bio + assert_equal 18, bob.age + + # Test for 30 significant digits (beyond the 16 of float), 10 of them + # after the decimal place. + + assert_equal BigDecimal("0012345678901234567890.0123456789"), bob.wealth + + assert_equal true, bob.male? + + assert_equal String, bob.first_name.class + assert_equal String, bob.last_name.class + assert_equal String, bob.bio.class + assert_kind_of Integer, bob.age + assert_equal Time, bob.birthday.class + assert_equal Date, bob.favorite_day.class + assert_instance_of TrueClass, bob.male? + assert_kind_of BigDecimal, bob.wealth + end + end + + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_limit_should_raise + assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 } + + unless current_adapter?(:PostgreSQLAdapter) + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff } + end + end + end + end + end +end diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb new file mode 100644 index 0000000000..1c62a68cf9 --- /dev/null +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class ColumnPositioningTest < ActiveRecord::TestCase + attr_reader :connection + alias :conn :connection + + def setup + super + + @connection = ActiveRecord::Base.connection + + connection.create_table :testings, id: false do |t| + t.column :first, :integer + t.column :second, :integer + t.column :third, :integer + end + end + + teardown do + connection.drop_table :testings rescue nil + ActiveRecord::Base.primary_key_prefix_type = nil + end + + if current_adapter?(:Mysql2Adapter) + def test_column_positioning + assert_equal %w(first second third), conn.columns(:testings).map(&:name) + end + + def test_add_column_with_positioning + conn.add_column :testings, :new_col, :integer + assert_equal %w(first second third new_col), conn.columns(:testings).map(&:name) + end + + def test_add_column_with_positioning_first + conn.add_column :testings, :new_col, :integer, first: true + assert_equal %w(new_col first second third), conn.columns(:testings).map(&:name) + end + + def test_add_column_with_positioning_after + conn.add_column :testings, :new_col, :integer, after: :first + assert_equal %w(first new_col second third), conn.columns(:testings).map(&:name) + end + + def test_change_column_with_positioning + conn.change_column :testings, :second, :integer, first: true + assert_equal %w(second first third), conn.columns(:testings).map(&:name) + + conn.change_column :testings, :second, :integer, after: :third + assert_equal %w(first third second), conn.columns(:testings).map(&:name) + end + + def test_add_reference_with_positioning_first + conn.add_reference :testings, :new, polymorphic: true, first: true + assert_equal %w(new_id new_type first second third), conn.columns(:testings).map(&:name) + end + + def test_add_reference_with_positioning_after + conn.add_reference :testings, :new, polymorphic: true, after: :first + assert_equal %w(first new_id new_type second third), conn.columns(:testings).map(&:name) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb new file mode 100644 index 0000000000..cedd9c44e3 --- /dev/null +++ b/activerecord/test/cases/migration/columns_test.rb @@ -0,0 +1,323 @@ +# frozen_string_literal: true + +require "cases/migration/helper" + +module ActiveRecord + class Migration + class ColumnsTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_tests = false + + # FIXME: this is more of an integration test with AR::Base and the + # schema modifications. Maybe we should move this? + def test_add_rename + add_column "test_models", "girlfriend", :string + TestModel.reset_column_information + + TestModel.create girlfriend: "bobette" + + rename_column "test_models", "girlfriend", "exgirlfriend" + + TestModel.reset_column_information + bob = TestModel.first + + assert_equal "bobette", bob.exgirlfriend + end + + # FIXME: another integration test. We should decouple this from the + # AR::Base implementation. + def test_rename_column_using_symbol_arguments + add_column :test_models, :first_name, :string + + TestModel.create first_name: "foo" + + rename_column :test_models, :first_name, :nick_name + TestModel.reset_column_information + assert_includes TestModel.column_names, "nick_name" + assert_equal ["foo"], TestModel.all.map(&:nick_name) + end + + # FIXME: another integration test. We should decouple this from the + # AR::Base implementation. + def test_rename_column + add_column "test_models", "first_name", "string" + + TestModel.create first_name: "foo" + + rename_column "test_models", "first_name", "nick_name" + TestModel.reset_column_information + assert_includes TestModel.column_names, "nick_name" + assert_equal ["foo"], TestModel.all.map(&:nick_name) + end + + def test_rename_column_preserves_default_value_not_null + add_column "test_models", "salary", :integer, default: 70000 + + default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default + assert_equal "70000", default_before + + rename_column "test_models", "salary", "annual_salary" + + assert_includes TestModel.column_names, "annual_salary" + default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default + assert_equal "70000", default_after + end + + if current_adapter?(:Mysql2Adapter) + def test_mysql_rename_column_preserves_auto_increment + rename_column "test_models", "id", "id_test" + assert_predicate connection.columns("test_models").find { |c| c.name == "id_test" }, :auto_increment? + TestModel.reset_column_information + ensure + rename_column "test_models", "id_test", "id" + end + end + + def test_rename_nonexistent_column + exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) + ActiveRecord::StatementInvalid + else + ActiveRecord::ActiveRecordError + end + + assert_raise(exception) do + rename_column "test_models", "nonexistent", "should_fail" + end + end + + def test_rename_column_with_sql_reserved_word + add_column "test_models", "first_name", :string + rename_column "test_models", "first_name", "group" + + assert_includes TestModel.column_names, "group" + end + + def test_rename_column_with_an_index + add_column "test_models", :hat_name, :string + add_index :test_models, :hat_name + + assert_equal 1, connection.indexes("test_models").size + rename_column "test_models", "hat_name", "name" + + assert_equal ["index_test_models_on_name"], connection.indexes("test_models").map(&:name) + end + + def test_rename_column_with_multi_column_index + add_column "test_models", :hat_size, :integer + add_column "test_models", :hat_style, :string, limit: 100 + add_index "test_models", ["hat_style", "hat_size"], unique: true + + rename_column "test_models", "hat_size", "size" + if current_adapter? :OracleAdapter + assert_equal ["i_test_models_hat_style_size"], connection.indexes("test_models").map(&:name) + else + assert_equal ["index_test_models_on_hat_style_and_size"], connection.indexes("test_models").map(&:name) + end + + rename_column "test_models", "hat_style", "style" + if current_adapter? :OracleAdapter + assert_equal ["i_test_models_style_size"], connection.indexes("test_models").map(&:name) + else + assert_equal ["index_test_models_on_style_and_size"], connection.indexes("test_models").map(&:name) + end + end + + def test_rename_column_does_not_rename_custom_named_index + add_column "test_models", :hat_name, :string + add_index :test_models, :hat_name, name: "idx_hat_name" + + assert_equal 1, connection.indexes("test_models").size + rename_column "test_models", "hat_name", "name" + assert_equal ["idx_hat_name"], connection.indexes("test_models").map(&:name) + end + + def test_remove_column_with_index + add_column "test_models", :hat_name, :string + add_index :test_models, :hat_name + + assert_equal 1, connection.indexes("test_models").size + remove_column("test_models", "hat_name") + assert_equal 0, connection.indexes("test_models").size + end + + def test_remove_column_with_multi_column_index + # MariaDB starting with 10.2.8 + # Dropping a column that is part of a multi-column UNIQUE constraint is not permitted. + skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.version >= "10.2.8" + + add_column "test_models", :hat_size, :integer + add_column "test_models", :hat_style, :string, limit: 100 + add_index "test_models", ["hat_style", "hat_size"], unique: true + + assert_equal 1, connection.indexes("test_models").size + remove_column("test_models", "hat_size") + + # Every database and/or database adapter has their own behavior + # if it drops the multi-column index when any of the indexed columns dropped by remove_column. + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) + assert_equal [], connection.indexes("test_models").map(&:name) + else + assert_equal ["index_test_models_on_hat_style_and_hat_size"], connection.indexes("test_models").map(&:name) + end + end + + def test_change_type_of_not_null_column + change_column "test_models", "updated_at", :datetime, null: false + change_column "test_models", "updated_at", :datetime, null: false + + TestModel.reset_column_information + assert_equal false, TestModel.columns_hash["updated_at"].null + ensure + change_column "test_models", "updated_at", :datetime, null: true + end + + def test_change_column_nullability + add_column "test_models", "funny", :boolean + assert TestModel.columns_hash["funny"].null, "Column 'funny' must initially allow nulls" + + change_column "test_models", "funny", :boolean, null: false, default: true + + TestModel.reset_column_information + assert_not TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point" + + change_column "test_models", "funny", :boolean, null: true + TestModel.reset_column_information + assert TestModel.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point" + end + + def test_change_column + add_column "test_models", "age", :integer + add_column "test_models", "approved", :boolean, default: true + + old_columns = connection.columns(TestModel.table_name) + + assert old_columns.find { |c| c.name == "age" && c.type == :integer } + + change_column "test_models", "age", :string + + new_columns = connection.columns(TestModel.table_name) + + assert_not new_columns.find { |c| c.name == "age" && c.type == :integer } + assert new_columns.find { |c| c.name == "age" && c.type == :string } + + old_columns = connection.columns(TestModel.table_name) + assert old_columns.find { |c| + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) + c.name == "approved" && c.type == :boolean && default == true + } + + change_column :test_models, :approved, :boolean, default: false + new_columns = connection.columns(TestModel.table_name) + + assert_not new_columns.find { |c| + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) + c.name == "approved" && c.type == :boolean && default == true + } + assert new_columns.find { |c| + default = connection.lookup_cast_type_from_column(c).deserialize(c.default) + c.name == "approved" && c.type == :boolean && default == false + } + change_column :test_models, :approved, :boolean, default: true + end + + def test_change_column_with_nil_default + add_column "test_models", "contributor", :boolean, default: true + assert_predicate TestModel.new, :contributor? + + change_column "test_models", "contributor", :boolean, default: nil + TestModel.reset_column_information + assert_not_predicate TestModel.new, :contributor? + assert_nil TestModel.new.contributor + end + + def test_change_column_to_drop_default_with_null_false + add_column "test_models", "contributor", :boolean, default: true, null: false + assert_predicate TestModel.new, :contributor? + + change_column "test_models", "contributor", :boolean, default: nil, null: false + TestModel.reset_column_information + assert_not_predicate TestModel.new, :contributor? + assert_nil TestModel.new.contributor + end + + def test_change_column_with_new_default + add_column "test_models", "administrator", :boolean, default: true + assert_predicate TestModel.new, :administrator? + + change_column "test_models", "administrator", :boolean, default: false + TestModel.reset_column_information + assert_not_predicate TestModel.new, :administrator? + end + + def test_change_column_with_custom_index_name + add_column "test_models", "category", :string + add_index :test_models, :category, name: "test_models_categories_idx" + + assert_equal ["test_models_categories_idx"], connection.indexes("test_models").map(&:name) + change_column "test_models", "category", :string, null: false, default: "article" + + assert_equal ["test_models_categories_idx"], connection.indexes("test_models").map(&:name) + end + + def test_change_column_with_long_index_name + table_name_prefix = "test_models_" + long_index_name = table_name_prefix + ("x" * (connection.allowed_index_name_length - table_name_prefix.length)) + add_column "test_models", "category", :string + add_index :test_models, :category, name: long_index_name + + change_column "test_models", "category", :string, null: false, default: "article" + + assert_equal [long_index_name], connection.indexes("test_models").map(&:name) + end + + def test_change_column_default + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", "Tester" + + assert_equal "Tester", TestModel.new.first_name + end + + def test_change_column_default_to_null + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", nil + assert_nil TestModel.new.first_name + end + + def test_change_column_default_with_from_and_to + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", from: nil, to: "Tester" + + assert_equal "Tester", TestModel.new.first_name + end + + def test_remove_column_no_second_parameter_raises_exception + assert_raise(ArgumentError) { connection.remove_column("funny") } + end + + def test_removing_and_renaming_column_preserves_custom_primary_key + connection.create_table "my_table", primary_key: "my_table_id", force: true do |t| + t.integer "col_one" + t.string "col_two", limit: 128, null: false + end + + remove_column("my_table", "col_two") + rename_column("my_table", "col_one", "col_three") + + assert_equal "my_table_id", connection.primary_key("my_table") + ensure + connection.drop_table(:my_table) rescue nil + end + + def test_column_with_index + connection.create_table "my_table", force: true do |t| + t.string :item_number, index: true + end + + assert connection.index_exists?("my_table", :item_number, name: :index_my_table_on_item_number) + ensure + connection.drop_table(:my_table) rescue nil + end + end + end +end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb new file mode 100644 index 0000000000..3a11bb081b --- /dev/null +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -0,0 +1,352 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class CommandRecorderTest < ActiveRecord::TestCase + def setup + connection = ActiveRecord::Base.connection + @recorder = CommandRecorder.new(connection) + end + + def test_respond_to_delegates + recorder = CommandRecorder.new(Class.new { + def america; end + }.new) + assert_respond_to recorder, :america + end + + def test_send_calls_super + assert_raises(NoMethodError) do + @recorder.send(:non_existing_method, :horses) + end + end + + def test_send_delegates_to_record + recorder = CommandRecorder.new(Class.new { + def create_table(name); end + }.new) + assert_respond_to recorder, :create_table + recorder.send(:create_table, :horses) + assert_equal [[:create_table, [:horses], nil]], recorder.commands + end + + def test_unknown_commands_delegate + recorder = Struct.new(:foo) + recorder = CommandRecorder.new(recorder.new("bar")) + assert_equal "bar", recorder.foo + end + + def test_inverse_of_raise_exception_on_unknown_commands + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :execute, ["some sql"] + end + end + + def test_irreversible_commands_raise_exception + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.revert { @recorder.execute "some sql" } + end + end + + def test_record + @recorder.record :create_table, [:system_settings] + assert_equal 1, @recorder.commands.length + end + + def test_inverted_commands_are_reversed + @recorder.revert do + @recorder.record :create_table, [:hello] + @recorder.record :create_table, [:world] + end + tables = @recorder.commands.map { |_cmd, args, _block| args } + assert_equal [[:world], [:hello]], tables + end + + def test_revert_order + block = Proc.new { |t| t.string :name } + @recorder.instance_eval do + create_table("apples", &block) + revert do + create_table("bananas", &block) + revert do + create_table("clementines", &block) + create_table("dates") + end + create_table("elderberries") + end + revert do + create_table("figs", &block) + create_table("grapes") + end + end + assert_equal [[:create_table, ["apples"], block], [:drop_table, ["elderberries"], nil], + [:create_table, ["clementines"], block], [:create_table, ["dates"], nil], + [:drop_table, ["bananas"], block], [:drop_table, ["grapes"], nil], + [:drop_table, ["figs"], block]], @recorder.commands + end + + def test_invert_change_table + @recorder.revert do + @recorder.change_table :fruits do |t| + t.string :name + t.rename :kind, :cultivar + end + end + assert_equal [ + [:rename_column, [:fruits, :cultivar, :kind]], + [:remove_column, [:fruits, :name, :string, {}], nil], + ], @recorder.commands + + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.revert do + @recorder.change_table :fruits do |t| + t.remove :kind + end + end + end + end + + def test_invert_create_table + @recorder.revert do + @recorder.record :create_table, [:system_settings] + end + drop_table = @recorder.commands.first + assert_equal [:drop_table, [:system_settings], nil], drop_table + end + + def test_invert_create_table_with_options_and_block + block = Proc.new {} + drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block + assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table + end + + def test_invert_drop_table + block = Proc.new {} + create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block + assert_equal [:create_table, [:people_reminders, id: false], block], create_table + end + + def test_invert_drop_table_without_a_block_nor_option + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :drop_table, [:people_reminders] + end + end + + def test_invert_create_join_table + drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists] + assert_equal [:drop_join_table, [:musics, :artists], nil], drop_join_table + end + + def test_invert_create_join_table_with_table_name + drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists, table_name: :catalog] + assert_equal [:drop_join_table, [:musics, :artists, table_name: :catalog], nil], drop_join_table + end + + def test_invert_drop_join_table + block = Proc.new {} + create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block + assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table + end + + def test_invert_rename_table + rename = @recorder.inverse_of :rename_table, [:old, :new] + assert_equal [:rename_table, [:new, :old]], rename + end + + def test_invert_add_column + remove = @recorder.inverse_of :add_column, [:table, :column, :type, {}] + assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove + end + + def test_invert_change_column + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column, [:table, :column, :type, {}] + end + end + + def test_invert_change_column_default + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column_default, [:table, :column, "default_value"] + end + end + + def test_invert_change_column_default_with_from_and_to + change = @recorder.inverse_of :change_column_default, [:table, :column, from: "old_value", to: "new_value"] + assert_equal [:change_column_default, [:table, :column, from: "new_value", to: "old_value"]], change + end + + def test_invert_change_column_default_with_from_and_to_with_boolean + change = @recorder.inverse_of :change_column_default, [:table, :column, from: true, to: false] + assert_equal [:change_column_default, [:table, :column, from: false, to: true]], change + end + + def test_invert_change_column_null + add = @recorder.inverse_of :change_column_null, [:table, :column, true] + assert_equal [:change_column_null, [:table, :column, false]], add + end + + def test_invert_remove_column + add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}] + assert_equal [:add_column, [:table, :column, :type, {}], nil], add + end + + def test_invert_remove_column_without_type + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :remove_column, [:table, :column] + end + end + + def test_invert_rename_column + rename = @recorder.inverse_of :rename_column, [:table, :old, :new] + assert_equal [:rename_column, [:table, :new, :old]], rename + end + + def test_invert_add_index + remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] + assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove + end + + def test_invert_add_index_with_name + remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"] + assert_equal [:remove_index, [:table, { name: "new_index" }]], remove + end + + def test_invert_add_index_with_algorithm_option + remove = @recorder.inverse_of :add_index, [:table, :one, algorithm: :concurrently] + assert_equal [:remove_index, [:table, { column: :one, algorithm: :concurrently }]], remove + end + + def test_invert_remove_index + add = @recorder.inverse_of :remove_index, [:table, :one] + assert_equal [:add_index, [:table, :one]], add + end + + def test_invert_remove_index_with_column + add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two], options: true }] + assert_equal [:add_index, [:table, [:one, :two], options: true]], add + end + + def test_invert_remove_index_with_name + add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two], name: "new_index" }] + assert_equal [:add_index, [:table, [:one, :two], name: "new_index"]], add + end + + def test_invert_remove_index_with_no_special_options + add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two] }] + assert_equal [:add_index, [:table, [:one, :two], {}]], add + end + + def test_invert_remove_index_with_no_column + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :remove_index, [:table, name: "new_index"] + end + end + + def test_invert_rename_index + rename = @recorder.inverse_of :rename_index, [:table, :old, :new] + assert_equal [:rename_index, [:table, :new, :old]], rename + end + + def test_invert_add_timestamps + remove = @recorder.inverse_of :add_timestamps, [:table] + assert_equal [:remove_timestamps, [:table], nil], remove + end + + def test_invert_remove_timestamps + add = @recorder.inverse_of :remove_timestamps, [:table, { null: true }] + assert_equal [:add_timestamps, [:table, { null: true }], nil], add + end + + def test_invert_add_reference + remove = @recorder.inverse_of :add_reference, [:table, :taggable, { polymorphic: true }] + assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }], nil], remove + end + + def test_invert_add_belongs_to_alias + remove = @recorder.inverse_of :add_belongs_to, [:table, :user] + assert_equal [:remove_reference, [:table, :user], nil], remove + end + + def test_invert_remove_reference + add = @recorder.inverse_of :remove_reference, [:table, :taggable, { polymorphic: true }] + assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add + end + + def test_invert_remove_reference_with_index_and_foreign_key + add = @recorder.inverse_of :remove_reference, [:table, :taggable, { index: true, foreign_key: true }] + assert_equal [:add_reference, [:table, :taggable, { index: true, foreign_key: true }], nil], add + end + + def test_invert_remove_belongs_to_alias + add = @recorder.inverse_of :remove_belongs_to, [:table, :user] + assert_equal [:add_reference, [:table, :user], nil], add + end + + def test_invert_enable_extension + disable = @recorder.inverse_of :enable_extension, ["uuid-ossp"] + assert_equal [:disable_extension, ["uuid-ossp"], nil], disable + end + + def test_invert_disable_extension + enable = @recorder.inverse_of :disable_extension, ["uuid-ossp"] + assert_equal [:enable_extension, ["uuid-ossp"], nil], enable + end + + def test_invert_add_foreign_key + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people] + assert_equal [:remove_foreign_key, [:dogs, :people]], enable + end + + def test_invert_remove_foreign_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people] + assert_equal [:add_foreign_key, [:dogs, :people]], enable + end + + def test_invert_add_foreign_key_with_column + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable + end + + def test_invert_remove_foreign_key_with_column + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id"]], enable + end + + def test_invert_add_foreign_key_with_column_and_name + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable + end + + def test_invert_remove_foreign_key_with_column_and_name + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]], enable + end + + def test_invert_remove_foreign_key_with_primary_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, primary_key: "person_id"] + assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable + end + + def test_invert_remove_foreign_key_with_on_delete_on_update + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade] + assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable + end + + def test_invert_remove_foreign_key_is_irreversible_without_to_table + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs] + end + end + end + end +end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb new file mode 100644 index 0000000000..69a50674af --- /dev/null +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -0,0 +1,369 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +module ActiveRecord + class Migration + class CompatibilityTest < ActiveRecord::TestCase + attr_reader :connection + self.use_transactional_tests = false + + def setup + super + @connection = ActiveRecord::Base.connection + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + connection.create_table :testings do |t| + t.column :foo, :string, limit: 5 + t.column :bar, :string, limit: 100 + end + end + + teardown do + connection.drop_table :testings rescue nil + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + def test_migration_doesnt_remove_named_index + connection.add_index :testings, :foo, name: "custom_index_name" + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :foo + end + }.new + + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + assert_raise(StandardError) { ActiveRecord::Migrator.new(:up, [migration]).migrate } + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + end + + def test_migration_does_remove_unnamed_index + connection.add_index :testings, :bar + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :bar + end + }.new + + assert connection.index_exists?(:testings, :bar) + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_not connection.index_exists?(:testings, :bar) + end + + def test_references_does_not_add_index_by_default + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + create_table :more_testings do |t| + t.references :foo + t.belongs_to :bar, index: false + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert_not connection.index_exists?(:more_testings, :foo_id) + assert_not connection.index_exists?(:more_testings, :bar_id) + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + create_table :more_testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:more_testings).find { |c| c.name == "created_at" }.null + assert connection.columns(:more_testings).find { |c| c.name == "updated_at" }.null + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + change_table :testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == "created_at" }.null + assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + add_timestamps :testings + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == "created_at" }.null + assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + end + + def test_legacy_migrations_raises_exception_when_inherited + e = assert_raises(StandardError) do + class_eval("class LegacyMigration < ActiveRecord::Migration; end") + end + assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) + end + + if current_adapter?(:PostgreSQLAdapter) + class Testing < ActiveRecord::Base + end + + def test_legacy_change_column_with_null_executes_update + migration = Class.new(ActiveRecord::Migration[5.1]) { + def migrate(x) + change_column :testings, :foo, :string, limit: 10, null: false, default: "foobar" + end + }.new + + Testing.create! + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_equal ["foobar"], Testing.all.map(&:foo) + ensure + ActiveRecord::Base.clear_cache! + end + end + end + end +end + +module LegacyPrimaryKeyTestCases + include SchemaDumpingHelper + + class LegacyPrimaryKey < ActiveRecord::Base + end + + def setup + @migration = nil + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + end + + def teardown + @migration.migrate(:down) if @migration + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + LegacyPrimaryKey.reset_column_information + end + + def test_legacy_primary_key_should_be_auto_incremented + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys do |t| + t.references :legacy_ref + end + end + }.new + + @migration.migrate(:up) + + assert_legacy_primary_key + + legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"] + assert_not_predicate legacy_ref, :bigint? + + record1 = LegacyPrimaryKey.create! + assert_not_nil record1.id + + record1.destroy + + record2 = LegacyPrimaryKey.create! + assert_not_nil record2.id + assert_operator record2.id, :>, record1.id + end + + def test_legacy_integer_primary_key_should_not_be_auto_incremented + skip if current_adapter?(:SQLite3Adapter) + + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: :integer do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema + end + + def test_legacy_primary_key_in_create_table_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.primary_key :id + end + end + }.new + + @migration.migrate(:up) + + assert_legacy_primary_key + end + + def test_legacy_primary_key_in_change_table_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.integer :dummy + end + change_table :legacy_primary_keys do |t| + t.primary_key :id + end + end + }.new + + @migration.migrate(:up) + + assert_legacy_primary_key + end + + def test_add_column_with_legacy_primary_key_should_be_integer + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: false do |t| + t.integer :dummy + end + add_column :legacy_primary_keys, :id, :primary_key + end + }.new + + @migration.migrate(:up) + + assert_legacy_primary_key + end + + def test_legacy_join_table_foreign_keys_should_be_integer + @migration = Class.new(migration_class) { + def change + create_join_table :apples, :bananas do |t| + end + end + }.new + + @migration.migrate(:up) + + schema = dump_table_schema "apples_bananas" + assert_match %r{integer "apple_id", null: false}, schema + assert_match %r{integer "banana_id", null: false}, schema + end + + def test_legacy_join_table_column_options_should_be_overwritten + @migration = Class.new(migration_class) { + def change + create_join_table :apples, :bananas, column_options: { type: :bigint } do |t| + end + end + }.new + + @migration.migrate(:up) + + schema = dump_table_schema "apples_bananas" + assert_match %r{bigint "apple_id", null: false}, schema + assert_match %r{bigint "banana_id", null: false}, schema + end + + if current_adapter?(:Mysql2Adapter) + def test_legacy_bigint_primary_key_should_be_auto_incremented + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: :bigint + end + }.new + + @migration.migrate(:up) + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + assert_predicate legacy_pk, :bigint? + assert_predicate legacy_pk, :auto_increment? + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema + end + else + def test_legacy_bigint_primary_key_should_not_be_auto_incremented + @migration = Class.new(migration_class) { + def change + create_table :legacy_primary_keys, id: :bigint do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema + end + end + + private + def assert_legacy_primary_key + assert_equal "id", LegacyPrimaryKey.primary_key + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + + assert_equal :integer, legacy_pk.type + assert_not_predicate legacy_pk, :bigint? + assert_not legacy_pk.null + + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema + end + end +end + +module LegacyPrimaryKeyTest + class V5_0 < ActiveRecord::TestCase + include LegacyPrimaryKeyTestCases + + self.use_transactional_tests = false + + private + def migration_class + ActiveRecord::Migration[5.0] + end + end + + class V4_2 < ActiveRecord::TestCase + include LegacyPrimaryKeyTestCases + + self.use_transactional_tests = false + + private + def migration_class + ActiveRecord::Migration[4.2] + end + end +end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb new file mode 100644 index 0000000000..83fb4f9385 --- /dev/null +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class CreateJoinTableTest < ActiveRecord::TestCase + attr_reader :connection + + def setup + super + @connection = ActiveRecord::Base.connection + end + + teardown do + %w(artists_musics musics_videos catalog).each do |table_name| + connection.drop_table table_name, if_exists: true + end + end + + def test_create_join_table + connection.create_join_table :artists, :musics + + assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort + end + + def test_create_join_table_set_not_null_by_default + connection.create_join_table :artists, :musics + + assert_equal [false, false], connection.columns(:artists_musics).map(&:null) + end + + def test_create_join_table_with_strings + connection.create_join_table "artists", "musics" + + assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort + end + + def test_create_join_table_with_symbol_and_string + connection.create_join_table :artists, "musics" + + assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort + end + + def test_create_join_table_with_the_proper_order + connection.create_join_table :videos, :musics + + assert_equal %w(music_id video_id), connection.columns(:musics_videos).map(&:name).sort + end + + def test_create_join_table_with_the_table_name + connection.create_join_table :artists, :musics, table_name: :catalog + + assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort + end + + def test_create_join_table_with_the_table_name_as_string + connection.create_join_table :artists, :musics, table_name: "catalog" + + assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort + end + + def test_create_join_table_with_column_options + connection.create_join_table :artists, :musics, column_options: { null: true } + + assert_equal [true, true], connection.columns(:artists_musics).map(&:null) + end + + def test_create_join_table_without_indexes + connection.create_join_table :artists, :musics + + assert_predicate connection.indexes(:artists_musics), :blank? + end + + def test_create_join_table_with_index + connection.create_join_table :artists, :musics do |t| + t.index [:artist_id, :music_id] + end + + assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns) + end + + def test_create_join_table_respects_reference_key_type + connection.create_join_table :artists, :musics do |t| + t.references :video + end + + artist_id, music_id, video_id = connection.columns(:artists_musics).sort_by(&:name) + + assert_equal video_id.sql_type, artist_id.sql_type + assert_equal video_id.sql_type, music_id.sql_type + end + + def test_drop_join_table + connection.create_join_table :artists, :musics + connection.drop_join_table :artists, :musics + + assert !connection.table_exists?("artists_musics") + end + + def test_drop_join_table_with_strings + connection.create_join_table :artists, :musics + connection.drop_join_table "artists", "musics" + + assert !connection.table_exists?("artists_musics") + end + + def test_drop_join_table_with_the_proper_order + connection.create_join_table :videos, :musics + connection.drop_join_table :videos, :musics + + assert !connection.table_exists?("musics_videos") + end + + def test_drop_join_table_with_the_table_name + connection.create_join_table :artists, :musics, table_name: :catalog + connection.drop_join_table :artists, :musics, table_name: :catalog + + assert !connection.table_exists?("catalog") + end + + def test_drop_join_table_with_the_table_name_as_string + connection.create_join_table :artists, :musics, table_name: "catalog" + connection.drop_join_table :artists, :musics, table_name: "catalog" + + assert !connection.table_exists?("catalog") + end + + def test_drop_join_table_with_column_options + connection.create_join_table :artists, :musics, column_options: { null: true } + connection.drop_join_table :artists, :musics, column_options: { null: true } + + assert !connection.table_exists?("artists_musics") + end + + def test_create_and_drop_join_table_with_common_prefix + with_table_cleanup do + connection.create_join_table "audio_artists", "audio_musics" + assert connection.table_exists?("audio_artists_musics") + + connection.drop_join_table "audio_artists", "audio_musics" + assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" + end + end + + if current_adapter?(:PostgreSQLAdapter) + def test_create_join_table_with_uuid + connection.create_join_table :artists, :musics, column_options: { type: :uuid } + assert_equal [:uuid, :uuid], connection.columns(:artists_musics).map(&:type) + end + end + + private + + def with_table_cleanup + tables_before = connection.data_sources + + yield + ensure + tables_after = connection.data_sources - tables_before + + tables_after.each do |table| + connection.drop_table table + end + end + end + end +end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb new file mode 100644 index 0000000000..de37215e80 --- /dev/null +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -0,0 +1,407 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? + module ActiveRecord + class Migration + class ForeignKeyInCreateTest < ActiveRecord::TestCase + def test_foreign_keys + foreign_keys = ActiveRecord::Base.connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter) + end + end + end + end +end + +if ActiveRecord::Base.connection.supports_foreign_keys? + module ActiveRecord + class Migration + class ForeignKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + include ActiveSupport::Testing::Stream + + class Rocket < ActiveRecord::Base + end + + class Astronaut < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets", force: true do |t| + t.string :name + end + + @connection.create_table "astronauts", force: true do |t| + t.string :name + t.references :rocket + end + end + + teardown do + @connection.drop_table "astronauts", if_exists: true + @connection.drop_table "rockets", if_exists: true + end + + def test_foreign_keys + foreign_keys = @connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name + end + + def test_add_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_equal("fk_rails_78146ddd2e", fk.name) + end + + def test_add_foreign_key_with_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_equal("fk_rails_78146ddd2e", fk.name) + end + + def test_add_foreign_key_with_non_standard_primary_key + @connection.create_table :space_shuttles, id: false, force: true do |t| + t.bigint :pk, primary_key: true + end + + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + ensure + @connection.remove_foreign_key :astronauts, name: "custom_pk" + @connection.drop_table :space_shuttles + end + + def test_add_on_delete_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + if current_adapter?(:Mysql2Adapter) + # ON DELETE RESTRICT is the default on MySQL + assert_nil fk.on_delete + else + assert_equal :restrict, fk.on_delete + end + end + + def test_add_on_delete_cascade_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :cascade, fk.on_delete + end + + def test_add_on_delete_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_delete + end + + def test_on_update_and_on_delete_raises_with_invalid_values + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid + end + + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid + end + end + + def test_add_foreign_key_with_on_update + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_update + end + + def test_foreign_key_exists + @connection.add_foreign_key :astronauts, :rockets + + assert @connection.foreign_key_exists?(:astronauts, :rockets) + assert_not @connection.foreign_key_exists?(:astronauts, :stars) + end + + def test_foreign_key_exists_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert @connection.foreign_key_exists?(:astronauts, column: "rocket_id") + assert_not @connection.foreign_key_exists?(:astronauts, column: "star_id") + end + + def test_foreign_key_exists_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk") + assert_not @connection.foreign_key_exists?(:astronauts, name: "other_fancy_named_fk") + end + + def test_remove_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, :rockets + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: "rocket_id" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_symbol_column + @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: :rocket_id + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.remove_foreign_key :astronauts, :rockets + end + end + + if ActiveRecord::Base.connection.supports_validate_constraints? + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_not_predicate fk, :validated? + end + + def test_validate_foreign_key_infers_column + @connection.add_foreign_key :astronauts, :rockets, validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, :rockets + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, column: "rocket_id" + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_key_by_symbol_column + @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, column: :rocket_id + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false + assert_not_predicate @connection.foreign_keys("astronauts").first, :validated? + + @connection.validate_foreign_key :astronauts, name: "fancy_named_fk" + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + + def test_validate_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.validate_foreign_key :astronauts, :rockets + end + end + + def test_validate_constraint_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false + + @connection.validate_constraint :astronauts, "fancy_named_fk" + assert_predicate @connection.foreign_keys("astronauts").first, :validated? + end + else + # Foreign key should still be created, but should not be invalid + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_predicate fk, :validated? + end + end + + def test_schema_dumping + @connection.add_foreign_key :astronauts, :rockets + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + end + + def test_schema_dumping_with_options + output = dump_table_schema "fk_test_has_fk" + assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output + end + + def test_schema_dumping_on_delete_and_on_update_options + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output + end + + class CreateCitiesAndHousesMigration < ActiveRecord::Migration::Current + def change + create_table("cities") { |t| } + + create_table("houses") do |t| + t.references :city + end + add_foreign_key :houses, :cities, column: "city_id" + + # remove and re-add to test that schema is updated and not accidentally cached + remove_foreign_key :houses, :cities + add_foreign_key :houses, :cities, column: "city_id", on_delete: :cascade + end + end + + def test_add_foreign_key_is_reversible + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("houses").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + + def test_foreign_key_constraint_is_not_cached_incorrectly + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + output = dump_table_schema "houses" + assert_match %r{\s+add_foreign_key "houses",.+on_delete: :cascade$}, output + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + + class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current + def change + create_table(:schools) + + create_table(:classes) do |t| + t.references :school + end + add_foreign_key :classes, :schools + end + end + + def test_add_foreign_key_with_prefix + ActiveRecord::Base.table_name_prefix = "p_" + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("p_classes").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_prefix = nil + end + + def test_add_foreign_key_with_suffix + ActiveRecord::Base.table_name_suffix = "_s" + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("classes_s").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_suffix = nil + end + end + end + end +else + module ActiveRecord + class Migration + class NoForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_add_foreign_key_should_be_noop + @connection.add_foreign_key :clubs, :categories + end + + def test_remove_foreign_key_should_be_noop + @connection.remove_foreign_key :clubs, :categories + end + + unless current_adapter?(:SQLite3Adapter) + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end + end + end + end + end + end +end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb new file mode 100644 index 0000000000..c056199140 --- /dev/null +++ b/activerecord/test/cases/migration/helper.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class << self; attr_accessor :message_count; end + self.message_count = 0 + + module TestHelper + attr_reader :connection, :table_name + + CONNECTION_METHODS = %w[add_column remove_column rename_column add_index change_column rename_table column_exists? index_exists? add_reference add_belongs_to remove_reference remove_references remove_belongs_to] + + class TestModel < ActiveRecord::Base + self.table_name = :test_models + end + + def setup + super + @connection = ActiveRecord::Base.connection + connection.create_table :test_models do |t| + t.timestamps null: true + end + + TestModel.reset_column_information + end + + def teardown + super + TestModel.reset_table_name + TestModel.reset_sequence_name + connection.drop_table :test_models, if_exists: true + end + + private + + delegate(*CONNECTION_METHODS, to: :connection) + end + end +end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb new file mode 100644 index 0000000000..b25c6d84bc --- /dev/null +++ b/activerecord/test/cases/migration/index_test.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class IndexTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + + connection.create_table table_name do |t| + t.column :foo, :string, limit: 100 + t.column :bar, :string, limit: 100 + + t.string :first_name + t.string :last_name, limit: 100 + t.string :key, limit: 100 + t.boolean :administrator + end + end + + teardown do + connection.drop_table :testings rescue nil + ActiveRecord::Base.primary_key_prefix_type = nil + end + + def test_rename_index + # keep the names short to make Oracle and similar behave + connection.add_index(table_name, [:foo], name: "old_idx") + connection.rename_index(table_name, "old_idx", "new_idx") + + assert_not connection.index_name_exists?(table_name, "old_idx") + assert connection.index_name_exists?(table_name, "new_idx") + end + + def test_rename_index_too_long + too_long_index_name = good_index_name + "x" + # keep the names short to make Oracle and similar behave + connection.add_index(table_name, [:foo], name: "old_idx") + e = assert_raises(ArgumentError) { + connection.rename_index(table_name, "old_idx", too_long_index_name) + } + assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) + + assert connection.index_name_exists?(table_name, "old_idx") + end + + def test_double_add_index + connection.add_index(table_name, [:foo], name: "some_idx") + assert_raises(ArgumentError) { + connection.add_index(table_name, [:foo], name: "some_idx") + } + end + + def test_remove_nonexistent_index + assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") } + end + + def test_add_index_works_with_long_index_names + connection.add_index(table_name, "foo", name: good_index_name) + + assert connection.index_name_exists?(table_name, good_index_name) + connection.remove_index(table_name, name: good_index_name) + end + + def test_add_index_does_not_accept_too_long_index_names + too_long_index_name = good_index_name + "x" + + e = assert_raises(ArgumentError) { + connection.add_index(table_name, "foo", name: too_long_index_name) + } + assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) + + assert_not connection.index_name_exists?(table_name, too_long_index_name) + connection.add_index(table_name, "foo", name: good_index_name) + end + + def test_internal_index_with_name_matching_database_limit + good_index_name = "x" * connection.index_name_length + connection.add_index(table_name, "foo", name: good_index_name, internal: true) + + assert connection.index_name_exists?(table_name, good_index_name) + connection.remove_index(table_name, name: good_index_name) + end + + def test_index_symbol_names + connection.add_index table_name, :foo, name: :symbol_index_name + assert connection.index_exists?(table_name, :foo, name: :symbol_index_name) + + connection.remove_index table_name, name: :symbol_index_name + assert_not connection.index_exists?(table_name, :foo, name: :symbol_index_name) + end + + def test_index_exists + connection.add_index :testings, :foo + + assert connection.index_exists?(:testings, :foo) + assert !connection.index_exists?(:testings, :bar) + end + + def test_index_exists_on_multiple_columns + connection.add_index :testings, [:foo, :bar] + + assert connection.index_exists?(:testings, [:foo, :bar]) + end + + def test_index_exists_with_custom_name_checks_columns + connection.add_index :testings, [:foo, :bar], name: "my_index" + assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index") + assert_not connection.index_exists?(:testings, [:foo], name: "my_index") + end + + def test_valid_index_options + assert_raise ArgumentError do + connection.add_index :testings, :foo, unqiue: true + end + end + + def test_unique_index_exists + connection.add_index :testings, :foo, unique: true + + assert connection.index_exists?(:testings, :foo, unique: true) + end + + def test_named_index_exists + connection.add_index :testings, :foo, name: "custom_index_name" + + assert connection.index_exists?(:testings, :foo) + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + assert !connection.index_exists?(:testings, :foo, name: "other_index_name") + end + + def test_remove_named_index + connection.add_index :testings, :foo, name: "custom_index_name" + + assert connection.index_exists?(:testings, :foo) + connection.remove_index :testings, :foo + assert !connection.index_exists?(:testings, :foo) + end + + def test_add_index_attribute_length_limit + connection.add_index :testings, [:foo, :bar], length: { foo: 10, bar: nil } + + assert connection.index_exists?(:testings, [:foo, :bar]) + end + + def test_add_index + connection.add_index("testings", "last_name") + connection.remove_index("testings", "last_name") + + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", column: ["last_name", "first_name"]) + + # Oracle adapter cannot have specified index name larger than 30 characters + # Oracle adapter is shortening index name when just column list is given + unless current_adapter?(:OracleAdapter) + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", name: :index_testings_on_last_name_and_first_name) + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", "last_name_and_first_name") + end + connection.add_index("testings", ["last_name", "first_name"]) + connection.remove_index("testings", ["last_name", "first_name"]) + + connection.add_index("testings", ["last_name"], length: 10) + connection.remove_index("testings", "last_name") + + connection.add_index("testings", ["last_name"], length: { last_name: 10 }) + connection.remove_index("testings", ["last_name"]) + + connection.add_index("testings", ["last_name", "first_name"], length: 10) + connection.remove_index("testings", ["last_name", "first_name"]) + + connection.add_index("testings", ["last_name", "first_name"], length: { last_name: 10, first_name: 20 }) + connection.remove_index("testings", ["last_name", "first_name"]) + + connection.add_index("testings", ["key"], name: "key_idx", unique: true) + connection.remove_index("testings", name: "key_idx", unique: true) + + connection.add_index("testings", %w(last_name first_name administrator), name: "named_admin") + connection.remove_index("testings", name: "named_admin") + + # Selected adapters support index sort order + if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) + connection.add_index("testings", ["last_name"], order: { last_name: :desc }) + connection.remove_index("testings", ["last_name"]) + connection.add_index("testings", ["last_name", "first_name"], order: { last_name: :desc }) + connection.remove_index("testings", ["last_name", "first_name"]) + connection.add_index("testings", ["last_name", "first_name"], order: { last_name: :desc, first_name: :asc }) + connection.remove_index("testings", ["last_name", "first_name"]) + connection.add_index("testings", ["last_name", "first_name"], order: :desc) + connection.remove_index("testings", ["last_name", "first_name"]) + end + end + + if current_adapter?(:PostgreSQLAdapter) + def test_add_partial_index + connection.add_index("testings", "last_name", where: "first_name = 'john doe'") + assert connection.index_exists?("testings", "last_name") + + connection.remove_index("testings", "last_name") + assert !connection.index_exists?("testings", "last_name") + end + end + + private + def good_index_name + "x" * connection.allowed_index_name_length + end + end + end +end diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb new file mode 100644 index 0000000000..28f4cc124b --- /dev/null +++ b/activerecord/test/cases/migration/logger_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class LoggerTest < ActiveRecord::TestCase + # MySQL can't roll back ddl changes + self.use_transactional_tests = false + + Migration = Struct.new(:name, :version) do + def disable_ddl_transaction; false end + def migrate(direction) + # do nothing + end + end + + def setup + super + ActiveRecord::SchemaMigration.create_table + ActiveRecord::SchemaMigration.delete_all + end + + teardown do + ActiveRecord::SchemaMigration.drop_table + end + + def test_migration_should_be_run_without_logger + previous_logger = ActiveRecord::Base.logger + ActiveRecord::Base.logger = nil + migrations = [Migration.new("a", 1), Migration.new("b", 2), Migration.new("c", 3)] + ActiveRecord::Migrator.new(:up, migrations).migrate + ensure + ActiveRecord::Base.logger = previous_logger + end + end + end +end diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb new file mode 100644 index 0000000000..dedb5ea502 --- /dev/null +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + if current_adapter?(:SQLite3Adapter) && !in_memory_db? + class PendingMigrationsTest < ActiveRecord::TestCase + setup do + file = ActiveRecord::Base.connection.raw_connection.filename + @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid" + source_db = SQLite3::Database.new file + dest_db = ActiveRecord::Base.connection.raw_connection + backup = SQLite3::Backup.new(dest_db, "main", source_db, "main") + backup.step(-1) + backup.finish + end + + teardown do + @conn.release_connection if @conn + ActiveRecord::Base.establish_connection :arunit + end + + def test_errors_if_pending + ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true + + assert_raises ActiveRecord::PendingMigrationError do + CheckPending.new(Proc.new {}).call({}) + end + end + + def test_checks_if_supported + ActiveRecord::SchemaMigration.create_table + migrator = Base.connection.migration_context + capture(:stdout) { migrator.migrate } + + assert_nil CheckPending.new(Proc.new {}).call({}) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb new file mode 100644 index 0000000000..7a092103c7 --- /dev/null +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true + +require "cases/helper" + +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? + module ActiveRecord + class Migration + class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true + end + + test "foreign keys can be created with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "no foreign key is created by default" do + @connection.create_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "foreign keys can be created in one query when index is not added" do + assert_queries(1) do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true, index: false + end + end + end + + test "options hash can be passed" do + @connection.change_table :testing_parents do |t| + t.references :other, index: { unique: true } + end + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "to_table option can be passed" do + @connection.create_table :testings do |t| + t.references :parent, foreign_key: { to_table: :testing_parents } + end + fks = @connection.foreign_keys("testings") + assert_equal([["testings", "testing_parents", "parent_id"]], + fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }) + end + end + end + end +end + +if ActiveRecord::Base.connection.supports_foreign_keys? + module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true + end + + test "foreign keys cannot be added to polymorphic relations when creating the table" do + @connection.create_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + + test "foreign keys can be created while changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "foreign keys are not added by default when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "foreign keys accept options when changing the table" do + @connection.change_table :testing_parents do |t| + t.references :other, index: { unique: true } + end + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + + test "foreign key column can be removed" do + @connection.create_table :testings do |t| + t.references :testing_parent, index: true, foreign_key: true + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_reference :testings, :testing_parent, foreign_key: true + end + end + + test "removing column removes foreign key" do + @connection.create_table :testings do |t| + t.references :testing_parent, index: true, foreign_key: true + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_column :testings, :testing_parent_id + end + end + + test "foreign key methods respect pluralize_table_names" do + begin + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + @connection.create_table :testing + @connection.change_table :testing_parents do |t| + t.references :testing, foreign_key: true + end + + fk = @connection.foreign_keys("testing_parents").first + assert_equal "testing_parents", fk.from_table + assert_equal "testing", fk.to_table + + assert_difference "@connection.foreign_keys('testing_parents').size", -1 do + @connection.remove_reference :testing_parents, :testing, foreign_key: true + end + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names + @connection.drop_table "testing", if_exists: true + end + end + + class CreateDogsMigration < ActiveRecord::Migration::Current + def change + create_table :dog_owners + + create_table :dogs do |t| + t.references :dog_owner, foreign_key: true + end + end + end + + def test_references_foreign_key_with_prefix + ActiveRecord::Base.table_name_prefix = "p_" + migration = CreateDogsMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("p_dogs").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_prefix = nil + end + + def test_references_foreign_key_with_suffix + ActiveRecord::Base.table_name_suffix = "_s" + migration = CreateDogsMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("dogs_s").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_suffix = nil + end + + test "multiple foreign keys can be added to the same table" do + @connection.create_table :testings do |t| + t.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } + end + + fks = @connection.foreign_keys("testings").sort_by(&:column) + + fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "parent1_id"], + ["testings", "testing_parents", "parent2_id"]], fk_definitions) + end + + test "multiple foreign keys can be removed to the selected one" do + @connection.create_table :testings do |t| + t.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_reference :testings, :parent1, foreign_key: { to_table: :testing_parents } + end + + fks = @connection.foreign_keys("testings").sort_by(&:column) + + fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "parent2_id"]], fk_definitions) + end + end + end + end +else + class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table("testings", if_exists: true) + @connection.drop_table("testing_parents", if_exists: true) + end + + test "ignores foreign keys defined with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + assert_includes @connection.data_sources, "testings" + end + end +end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb new file mode 100644 index 0000000000..e41377d817 --- /dev/null +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class Migration + class ReferencesIndexTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + teardown do + connection.drop_table :testings rescue nil + end + + def test_creates_index + connection.create_table table_name do |t| + t.references :foo, index: true + end + + assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id) + end + + def test_creates_index_by_default_even_if_index_option_is_not_passed + connection.create_table table_name do |t| + t.references :foo + end + + assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id) + end + + def test_does_not_create_index_explicit + connection.create_table table_name do |t| + t.references :foo, index: false + end + + assert_not connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id) + end + + def test_creates_index_with_options + connection.create_table table_name do |t| + t.references :foo, index: { name: :index_testings_on_yo_momma } + t.references :bar, index: { unique: true } + end + + assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_yo_momma) + assert connection.index_exists?(table_name, :bar_id, name: :index_testings_on_bar_id, unique: true) + end + + unless current_adapter? :OracleAdapter + def test_creates_polymorphic_index + connection.create_table table_name do |t| + t.references :foo, polymorphic: true, index: true + end + + assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id) + end + end + + def test_creates_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, index: true + end + + assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id) + end + + def test_creates_index_for_existing_table_even_if_index_option_is_not_passed + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo + end + + assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id) + end + + def test_does_not_create_index_for_existing_table_explicit + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, index: false + end + + assert_not connection.index_exists?(table_name, :foo_id, name: :index_testings_on_foo_id) + end + + unless current_adapter? :OracleAdapter + def test_creates_polymorphic_index_for_existing_table + connection.create_table table_name + connection.change_table table_name do |t| + t.references :foo, polymorphic: true, index: true + end + + assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb new file mode 100644 index 0000000000..769241ba12 --- /dev/null +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require "cases/migration/helper" + +module ActiveRecord + class Migration + class ReferencesStatementsTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_tests = false + + def setup + super + @table_name = :test_models + + add_column table_name, :supplier_id, :integer + add_index table_name, :supplier_id + end + + def test_creates_reference_id_column + add_reference table_name, :user + assert column_exists?(table_name, :user_id, :integer) + end + + def test_does_not_create_reference_type_column + add_reference table_name, :taggable + assert_not column_exists?(table_name, :taggable_type, :string) + end + + def test_creates_reference_type_column + add_reference table_name, :taggable, polymorphic: true + assert column_exists?(table_name, :taggable_type, :string) + end + + def test_does_not_create_reference_id_index_if_index_is_false + add_reference table_name, :user, index: false + assert_not index_exists?(table_name, :user_id) + end + + def test_create_reference_id_index_even_if_index_option_is_not_passed + add_reference table_name, :user + assert index_exists?(table_name, :user_id) + end + + def test_creates_polymorphic_index + add_reference table_name, :taggable, polymorphic: true, index: true + assert index_exists?(table_name, [:taggable_type, :taggable_id]) + end + + def test_creates_reference_type_column_with_default + add_reference table_name, :taggable, polymorphic: { default: "Photo" }, index: true + assert column_exists?(table_name, :taggable_type, :string, default: "Photo") + end + + def test_creates_reference_type_column_with_not_null + connection.create_table table_name, force: true do |t| + t.references :taggable, null: false, polymorphic: true + end + assert column_exists?(table_name, :taggable_id, :integer, null: false) + assert column_exists?(table_name, :taggable_type, :string, null: false) + end + + def test_does_not_share_options_with_reference_type_column + add_reference table_name, :taggable, type: :integer, limit: 2, polymorphic: true + assert column_exists?(table_name, :taggable_id, :integer, limit: 2) + assert column_exists?(table_name, :taggable_type, :string) + assert_not column_exists?(table_name, :taggable_type, :string, limit: 2) + end + + def test_creates_named_index + add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id" } + assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id") + end + + def test_creates_named_unique_index + add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id", unique: true } + assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id", unique: true) + end + + def test_creates_reference_id_with_specified_type + add_reference table_name, :user, type: :string + assert column_exists?(table_name, :user_id, :string) + end + + def test_deletes_reference_id_column + remove_reference table_name, :supplier + assert_not column_exists?(table_name, :supplier_id, :integer) + end + + def test_deletes_reference_id_index + remove_reference table_name, :supplier + assert_not index_exists?(table_name, :supplier_id) + end + + def test_does_not_delete_reference_type_column + with_polymorphic_column do + remove_reference table_name, :supplier + + assert_not column_exists?(table_name, :supplier_id, :integer) + assert column_exists?(table_name, :supplier_type, :string) + end + end + + def test_deletes_reference_type_column + with_polymorphic_column do + remove_reference table_name, :supplier, polymorphic: true + assert_not column_exists?(table_name, :supplier_type, :string) + end + end + + def test_deletes_polymorphic_index + with_polymorphic_column do + remove_reference table_name, :supplier, polymorphic: true + assert_not index_exists?(table_name, [:supplier_id, :supplier_type]) + end + end + + def test_add_belongs_to_alias + add_belongs_to table_name, :user + assert column_exists?(table_name, :user_id, :integer) + end + + def test_remove_belongs_to_alias + remove_belongs_to table_name, :supplier + assert_not column_exists?(table_name, :supplier_id, :integer) + end + + private + + def with_polymorphic_column + add_column table_name, :supplier_type, :string + add_index table_name, [:supplier_id, :supplier_type] + + yield + end + end + end +end diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb new file mode 100644 index 0000000000..a9deb92585 --- /dev/null +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require "cases/migration/helper" + +module ActiveRecord + class Migration + class RenameTableTest < ActiveRecord::TestCase + include ActiveRecord::Migration::TestHelper + + self.use_transactional_tests = false + + def setup + super + add_column "test_models", :url, :string + remove_column "test_models", :created_at + remove_column "test_models", :updated_at + end + + def teardown + rename_table :octopi, :test_models if connection.table_exists? :octopi + super + end + + if current_adapter?(:SQLite3Adapter) + def test_rename_table_for_sqlite_should_work_with_reserved_words + renamed = false + + add_column :test_models, :url, :string + connection.rename_table :references, :old_references + connection.rename_table :test_models, :references + + renamed = true + + # Using explicit id in insert for compatibility across all databases + connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)" + assert_equal "http://rubyonrails.com", connection.select_value("SELECT url FROM 'references' WHERE id=1") + ensure + return unless renamed + connection.rename_table :references, :test_models + connection.rename_table :old_references, :references + end + end + + unless current_adapter?(:FbAdapter) # Firebird cannot rename tables + def test_rename_table + rename_table :test_models, :octopi + + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + + assert_equal "http://www.foreverflying.com/octopus-black7.jpg", connection.select_value("SELECT url FROM octopi WHERE id=1") + end + + def test_rename_table_with_an_index + add_index :test_models, :url + + rename_table :test_models, :octopi + + connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')" + + assert_equal "http://www.foreverflying.com/octopus-black7.jpg", connection.select_value("SELECT url FROM octopi WHERE id=1") + index = connection.indexes(:octopi).first + assert_includes index.columns, "url" + assert_equal "index_octopi_on_url", index.name + end + + def test_rename_table_does_not_rename_custom_named_index + add_index :test_models, :url, name: "special_url_idx" + + rename_table :test_models, :octopi + + assert_equal ["special_url_idx"], connection.indexes(:octopi).map(&:name) + end + end + + if current_adapter?(:PostgreSQLAdapter) + def test_rename_table_for_postgresql_should_also_rename_default_sequence + rename_table :test_models, :octopi + + pk, seq = connection.pk_and_sequence_for("octopi") + + assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq + end + + def test_renaming_table_renames_primary_key + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" + rename_table :cats, :felines + + assert connection.table_exists? :felines + assert_not connection.table_exists? :cats + + primary_key_name = connection.select_values(<<~SQL, "SCHEMA")[0] + SELECT c.relname + FROM pg_class c + JOIN pg_index i + ON c.oid = i.indexrelid + WHERE i.indisprimary + AND i.indrelid = 'felines'::regclass + SQL + + assert_equal "felines_pkey", primary_key_name + ensure + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true + end + + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" + assert_nothing_raised { rename_table :cats, :felines } + assert connection.table_exists? :felines + assert_not connection.table_exists? :cats + ensure + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true + end + end + end + end +end |