diff options
Diffstat (limited to 'activerecord/test')
218 files changed, 6794 insertions, 2183 deletions
diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index f977b2997b..f1f457aedd 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -32,7 +32,8 @@ module ActiveRecord name.to_s, options[:default], fetch_type_metadata(sql_type), - options[:null]) + options[:null], + ) end def columns(table_name) diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 64c2b51f83..0bc617edbe 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -12,6 +12,7 @@ module ActiveRecord def setup @connection = ActiveRecord::Base.connection @connection.materialize_transactions + @connection_handler = ActiveRecord::Base.connection_handler end ## @@ -109,6 +110,11 @@ module ActiveRecord end end + def test_exec_query_returns_an_empty_result + result = @connection.exec_query "INSERT INTO subscribers(nick) VALUES('me')" + assert_instance_of(ActiveRecord::Result, result) + end + if current_adapter?(:Mysql2Adapter) def test_charset assert_not_nil @connection.charset @@ -127,19 +133,17 @@ module ActiveRecord end def test_not_specifying_database_name_for_cross_database_selects - begin - assert_nothing_raised do - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"].except(:database)) - - config = ARTest.connection_config - ActiveRecord::Base.connection.execute( - "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \ - "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses" - ) - end - ensure - ActiveRecord::Base.establish_connection :arunit + assert_nothing_raised do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"].except(:database)) + + config = ARTest.connection_config + ActiveRecord::Base.connection.execute( + "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \ + "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses" + ) end + ensure + ActiveRecord::Base.establish_connection :arunit end end @@ -160,6 +164,65 @@ module ActiveRecord end end + def test_preventing_writes_predicate + assert_not_predicate @connection, :preventing_writes? + + @connection_handler.while_preventing_writes do + assert_predicate @connection, :preventing_writes? + end + + assert_not_predicate @connection, :preventing_writes? + end + + def test_errors_when_an_insert_query_is_called_while_preventing_writes + assert_no_queries do + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @connection.transaction do + @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false) + end + end + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')") + + assert_no_queries do + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @connection.transaction do + @connection.update("UPDATE subscribers SET nick = '9989' WHERE nick = '138853948594'") + end + end + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')") + + assert_no_queries do + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @connection.transaction do + @connection.delete("DELETE FROM subscribers WHERE nick = '138853948594'") + end + end + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')") + + @connection_handler.while_preventing_writes do + result = @connection.select_all("SELECT subscribers.* FROM subscribers WHERE nick = '138853948594'") + assert_equal 1, result.length + end + end + def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" error = assert_raises(ActiveRecord::RecordNotUnique) do @@ -286,16 +349,8 @@ module ActiveRecord assert_equal "special_db_type", @connection.type_to_sql(:special_db_type) end - unless current_adapter?(:PostgreSQLAdapter) - def test_log_invalid_encoding - error = assert_raises RuntimeError do - @connection.send :log, "SELECT 'ы' FROM DUAL" do - raise (+"ы").force_encoding(Encoding::ASCII_8BIT) - end - end - - assert_equal "ы", error.message - end + def test_supports_foreign_keys_in_create_is_deprecated + assert_deprecated { @connection.supports_foreign_keys_in_create? } end def test_supports_multi_insert_is_deprecated @@ -398,19 +453,21 @@ module ActiveRecord class AdapterTestWithoutTransaction < ActiveRecord::TestCase self.use_transactional_tests = false - class Klass < ActiveRecord::Base - end + fixtures :posts, :authors, :author_addresses def setup - Klass.establish_connection :arunit - @connection = Klass.connection - end - - teardown do - Klass.remove_connection + @connection = ActiveRecord::Base.connection end unless in_memory_db? + test "reconnect after a disconnect" do + assert_predicate @connection, :active? + @connection.disconnect! + assert_not_predicate @connection, :active? + @connection.reconnect! + assert_predicate @connection, :active? + end + test "transaction state is reset after a reconnect" do @connection.begin_transaction assert_predicate @connection, :transaction_open? @@ -423,9 +480,65 @@ module ActiveRecord assert_predicate @connection, :transaction_open? @connection.disconnect! assert_not_predicate @connection, :transaction_open? + ensure + @connection.reconnect! end end + def test_truncate + assert_operator Post.count, :>, 0 + + @connection.truncate("posts") + + assert_equal 0, Post.count + ensure + reset_fixtures("posts") + end + + def test_truncate_with_query_cache + @connection.enable_query_cache! + + assert_operator Post.count, :>, 0 + + @connection.truncate("posts") + + assert_equal 0, Post.count + ensure + reset_fixtures("posts") + @connection.disable_query_cache! + end + + def test_truncate_tables + assert_operator Post.count, :>, 0 + assert_operator Author.count, :>, 0 + assert_operator AuthorAddress.count, :>, 0 + + @connection.truncate_tables("author_addresses", "authors", "posts") + + assert_equal 0, Post.count + assert_equal 0, Author.count + assert_equal 0, AuthorAddress.count + ensure + reset_fixtures("posts", "authors", "author_addresses") + end + + def test_truncate_tables_with_query_cache + @connection.enable_query_cache! + + assert_operator Post.count, :>, 0 + assert_operator Author.count, :>, 0 + assert_operator AuthorAddress.count, :>, 0 + + @connection.truncate_tables("author_addresses", "authors", "posts") + + assert_equal 0, Post.count + assert_equal 0, Author.count + assert_equal 0, AuthorAddress.count + ensure + reset_fixtures("posts", "authors", "author_addresses") + @connection.disable_query_cache! + end + # test resetting sequences in odd tables in PostgreSQL if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!) require "models/movie" @@ -445,6 +558,15 @@ module ActiveRecord assert_nothing_raised { sub.save! } end end + + private + def reset_fixtures(*fixture_names) + ActiveRecord::FixtureSet.reset_cache + + fixture_names.each do |fixture_name| + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, fixture_name) + end + end end end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 261fee13eb..c2c357d0c1 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -7,9 +7,18 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase include ConnectionHelper def setup + ActiveRecord::Base.connection.send(:default_row_format) ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_without_stub, :execute - def execute(sql, name = nil) sql end + def execute(sql, name = nil) + ActiveSupport::Notifications.instrumenter.instrument( + "sql.active_record", + sql: sql, + name: name, + connection: self) do + sql + end + end end end @@ -68,18 +77,18 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`))" + expected = /\ACREATE TABLE `people` \(#{type} INDEX `index_people_on_last_name` \(`last_name`\)\)/ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, type: type end - assert_equal expected, actual + assert_match expected, actual end - expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)))" + expected = /\ACREATE TABLE `people` \( INDEX `index_people_on_last_name` USING btree \(`last_name`\(10\)\)\)/ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, length: 10, using: :btree end - assert_equal expected, actual + assert_match expected, actual end def test_index_in_bulk_change @@ -88,17 +97,19 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" - actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| - t.index :last_name, type: type + assert_sql(expected) do + ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, type: type + end end - assert_equal expected, actual end expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" - actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| - t.index :last_name, length: 10, using: :btree, algorithm: :copy + assert_sql(expected) do + ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, length: 10, using: :btree, algorithm: :copy + end end - assert_equal expected, actual end def test_drop_table @@ -106,7 +117,13 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt) + if row_format_dynamic_by_default? + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt) + else + error = assert_raises(RuntimeError) { create_database(:matt) } + expected = "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns." + assert_equal expected, error.message + end assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1") assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin") end @@ -130,29 +147,25 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def test_add_timestamps with_real_execute do - begin - ActiveRecord::Base.connection.create_table :delete_me - ActiveRecord::Base.connection.add_timestamps :delete_me, null: true - assert column_present?("delete_me", "updated_at", "datetime") - assert column_present?("delete_me", "created_at", "datetime") - ensure - ActiveRecord::Base.connection.drop_table :delete_me rescue nil - end + ActiveRecord::Base.connection.create_table :delete_me + ActiveRecord::Base.connection.add_timestamps :delete_me, null: true + assert column_exists?("delete_me", "updated_at", "datetime") + assert column_exists?("delete_me", "created_at", "datetime") + ensure + ActiveRecord::Base.connection.drop_table :delete_me rescue nil end end def test_remove_timestamps with_real_execute do - begin - ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps null: true - end - ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true - assert_not column_present?("delete_me", "updated_at", "datetime") - assert_not column_present?("delete_me", "created_at", "datetime") - ensure - ActiveRecord::Base.connection.drop_table :delete_me rescue nil + ActiveRecord::Base.connection.create_table :delete_me do |t| + t.timestamps null: true end + ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true + assert_not column_exists?("delete_me", "updated_at", "datetime") + assert_not column_exists?("delete_me", "created_at", "datetime") + ensure + ActiveRecord::Base.connection.drop_table :delete_me rescue nil end end @@ -163,12 +176,12 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase [:temp], returns: false ) do - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) AS SELECT id, name, zip FROM a_really_complicated_query" + expected = /\ACREATE TEMPORARY TABLE `temp` \( INDEX `index_temp_on_zip` \(`zip`\)\)(?: ROW_FORMAT=DYNAMIC)? AS SELECT id, name, zip FROM a_really_complicated_query/ actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| t.index :zip end - assert_equal expected, actual + assert_match expected, actual end end @@ -191,9 +204,4 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) end - - def column_present?(table_name, column_name, type) - results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'") - results.first && results.first["Type"] == type - end end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index c32475c683..3756f74c95 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -22,7 +22,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: false) CollationTest.create!(string_ci_column: "A") invalid = CollationTest.new(string_ci_column: "a") - queries = assert_sql { invalid.save } + queries = capture_sql { invalid.save } ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } assert_no_match(/lower/i, ci_uniqueness_query) end @@ -31,7 +31,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: false) CollationTest.create!(string_cs_column: "A") invalid = CollationTest.new(string_cs_column: "a") - queries = assert_sql { invalid.save } + queries = capture_sql { invalid.save } cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_match(/lower/i, cs_uniqueness_query) end @@ -40,7 +40,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: true) CollationTest.create!(string_ci_column: "A") invalid = CollationTest.new(string_ci_column: "A") - queries = assert_sql { invalid.save } + queries = capture_sql { invalid.save } ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } assert_match(/binary/i, ci_uniqueness_query) end @@ -49,7 +49,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: true) CollationTest.create!(string_cs_column: "A") invalid = CollationTest.new(string_cs_column: "A") - queries = assert_sql { invalid.save } + queries = capture_sql { invalid.save } cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_no_match(/binary/i, cs_uniqueness_query) end @@ -58,7 +58,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase CollationTest.validates_uniqueness_of(:binary_column, case_sensitive: true) CollationTest.create!(binary_column: "A") invalid = CollationTest.new(binary_column: "A") - queries = assert_sql { invalid.save } + queries = capture_sql { invalid.save } bin_uniqueness_query = queries.detect { |q| q.match(/binary_column/) } assert_no_match(/\bBINARY\b/, bin_uniqueness_query) end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 3103589186..cb7461a8d5 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -28,17 +28,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end end - def test_truncate - rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") - count = rows.first.values.first - assert_operator count, :>, 0 - - ActiveRecord::Base.connection.truncate("comments") - rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") - count = rows.first.values.first - assert_equal 0, count - end - def test_no_automatic_reconnection_after_timeout assert_predicate @connection, :active? @connection.update("set @@wait_timeout=1") @@ -208,7 +197,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end private - def test_lock_free(lock_name) @connection.select_value("SELECT IS_FREE_LOCK(#{@connection.quote(lock_name)})") == 1 end diff --git a/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb new file mode 100644 index 0000000000..4d361e405c --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" +require "models/author" +require "models/bulb" + +module ActiveRecord + class CountDeletedRowsWithLockTest < ActiveRecord::Mysql2TestCase + test "delete and create in different threads synchronize correctly" do + Bulb.unscoped.delete_all + Bulb.create!(name: "Jimmy", color: "blue") + + delete_thread = Thread.new do + Bulb.unscoped.delete_all + end + + create_thread = Thread.new do + Author.create!(name: "Tommy") + end + + delete_thread.join + create_thread.join + + assert_equal 1, delete_thread.value + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb index 00a075e063..cbe55f1d53 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -46,10 +46,7 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase def stub_version(full_version_string) @connection.stub(:full_version, full_version_string) do - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) yield end - ensure - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) end end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 832f5d61d1..1168b3677e 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -1,11 +1,20 @@ # frozen_string_literal: true require "cases/helper" +require "support/schema_dumping_helper" class Mysql2EnumTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + class EnumTest < ActiveRecord::Base end + def setup + EnumTest.connection.create_table :enum_tests, id: false, force: true do |t| + t.column :enum_column, "enum('text','blob','tiny','medium','long','unsigned','bigint')" + end + end + def test_enum_limit column = EnumTest.columns_hash["enum_column"] assert_equal 8, column.limit @@ -20,4 +29,9 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase column = EnumTest.columns_hash["enum_column"] assert_not_predicate column, :bigint? end + + def test_schema_dumping + schema = dump_table_schema "enum_tests" + assert_match %r{t\.column "enum_column", "enum\('text','blob','tiny','medium','long','unsigned','bigint'\)"$}, schema + end end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index 0719baaa23..cfc1823773 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -8,6 +8,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase def setup @conn = ActiveRecord::Base.connection + @connection_handler = ActiveRecord::Base.connection_handler end def test_exec_query_nothing_raises_with_no_result_queries @@ -19,6 +20,18 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end end + def test_database_exists_returns_false_if_database_does_not_exist + config = ActiveRecord::Base.configurations["arunit"].merge(database: "inexistent_activerecord_unittest") + assert_not ActiveRecord::ConnectionAdapters::Mysql2Adapter.database_exists?(config), + "expected database to not exist" + end + + def test_database_exists_returns_true_when_the_database_exists + config = ActiveRecord::Base.configurations["arunit"] + assert ActiveRecord::ConnectionAdapters::Mysql2Adapter.database_exists?(config), + "expected database #{config[:database]} to exist" + end + def test_columns_for_distinct_zero_orders assert_equal "posts.id", @conn.columns_for_distinct("posts.id", []) @@ -56,7 +69,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase @conn.columns_for_distinct("posts.id", [order]) end - def test_errors_for_bigint_fks_on_integer_pk_table + def test_errors_for_bigint_fks_on_integer_pk_table_in_alter_table # table old_cars has primary key of integer error = assert_raises(ActiveRecord::MismatchedForeignKey) do @@ -64,13 +77,170 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase @conn.add_foreign_key :engines, :old_cars end - assert_match "Column `old_car_id` on table `engines` has a type of `bigint(20)`", error.message + assert_includes error.message, <<~MSG.squish + Column `old_car_id` on table `engines` does not match column `id` on `old_cars`, + which has type `int(11)`. To resolve this issue, change the type of the `old_car_id` + column on `engines` to be :integer. (For example `t.integer :old_car_id`). + MSG assert_not_nil error.cause - @conn.exec_query("ALTER TABLE engines DROP COLUMN old_car_id") + ensure + @conn.execute("ALTER TABLE engines DROP COLUMN old_car_id") rescue nil end - private + def test_errors_for_bigint_fks_on_integer_pk_table_in_create_table + # table old_cars has primary key of integer + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.execute(<<~SQL) + CREATE TABLE activerecord_unittest.foos ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + old_car_id bigint, + INDEX index_foos_on_old_car_id (old_car_id), + CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (old_car_id) REFERENCES old_cars (id) + ) + SQL + end + + assert_includes error.message, <<~MSG.squish + Column `old_car_id` on table `foos` does not match column `id` on `old_cars`, + which has type `int(11)`. To resolve this issue, change the type of the `old_car_id` + column on `foos` to be :integer. (For example `t.integer :old_car_id`). + MSG + assert_not_nil error.cause + ensure + @conn.drop_table :foos, if_exists: true + end + + def test_errors_for_integer_fks_on_bigint_pk_table_in_create_table + # table old_cars has primary key of bigint + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.execute(<<~SQL) + CREATE TABLE activerecord_unittest.foos ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + car_id int, + INDEX index_foos_on_car_id (car_id), + CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (car_id) REFERENCES cars (id) + ) + SQL + end + + assert_includes error.message, <<~MSG.squish + Column `car_id` on table `foos` does not match column `id` on `cars`, + which has type `bigint(20)`. To resolve this issue, change the type of the `car_id` + column on `foos` to be :bigint. (For example `t.bigint :car_id`). + MSG + assert_not_nil error.cause + ensure + @conn.drop_table :foos, if_exists: true + end + + def test_errors_for_bigint_fks_on_string_pk_table_in_create_table + # table old_cars has primary key of string + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.execute(<<~SQL) + CREATE TABLE activerecord_unittest.foos ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + subscriber_id bigint, + INDEX index_foos_on_subscriber_id (subscriber_id), + CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (subscriber_id) REFERENCES subscribers (nick) + ) + SQL + end + + assert_includes error.message, <<~MSG.squish + Column `subscriber_id` on table `foos` does not match column `nick` on `subscribers`, + which has type `varchar(255)`. To resolve this issue, change the type of the `subscriber_id` + column on `foos` to be :string. (For example `t.string :subscriber_id`). + MSG + assert_not_nil error.cause + ensure + @conn.drop_table :foos, if_exists: true + end + + def test_errors_when_an_insert_query_is_called_while_preventing_writes + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.update("UPDATE `engines` SET `engines`.`car_id` = '9989' WHERE `engines`.`car_id` = '138853948594'") + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.execute("DELETE FROM `engines` where `engines`.`car_id` = '138853948594'") + end + end + end + + def test_errors_when_a_replace_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.execute("REPLACE INTO `engines` SET `engines`.`car_id` = '249823948'") + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + @connection_handler.while_preventing_writes do + assert_equal 1, @conn.execute("SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594'").entries.count + end + end + + def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes + @connection_handler.while_preventing_writes do + assert_equal 2, @conn.execute("SHOW FULL FIELDS FROM `engines`").entries.count + end + end + + def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes + @connection_handler.while_preventing_writes do + assert_nil @conn.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci") + end + end + + def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + @connection_handler.while_preventing_writes do + assert_equal 1, @conn.execute("(\n( SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594' ) )").entries.count + end + end + + def test_read_timeout_exception + ActiveRecord::Base.establish_connection( + ActiveRecord::Base.configurations[:arunit].merge("read_timeout" => 1) + ) + + error = assert_raises(ActiveRecord::AdapterTimeout) do + ActiveRecord::Base.connection.execute("SELECT SLEEP(2)") + end + assert_kind_of ActiveRecord::QueryAborted, error + + assert_equal Mysql2::Error::TimeoutError, error.cause.class + ensure + ActiveRecord::Base.establish_connection :arunit + end + + private def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block) super(@conn, "ex", definition, &block) end diff --git a/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb new file mode 100644 index 0000000000..628802b216 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +if supports_optimizer_hints? + class Mysql2OptimzerHintsTest < ActiveRecord::Mysql2TestCase + fixtures :posts + + def test_optimizer_hints + assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do + posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |" + end + end + + def test_optimizer_hints_with_count_subquery + assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do + posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)") + posts = posts.select(:id).where(author_id: [0, 1]).limit(5) + assert_equal 5, posts.count + end + end + + def test_optimizer_hints_is_sanitized + assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do + posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |" + end + + assert_sql(%r{\ASELECT /\*\+ `posts`\.\*, \*/}) do + posts = Post.optimizer_hints("**// `posts`.*, //**") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_equal({ "id" => 1 }, posts.first.as_json) + end + end + + def test_optimizer_hints_with_unscope + assert_sql(%r{\ASELECT `posts`\.`id`}) do + posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */") + posts = posts.select(:id).where(author_id: [0, 1]) + posts.unscope(:optimizer_hints).load + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index d7d9a2d732..182d5a3e58 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -40,7 +40,6 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase end private - def with_encoding_utf8mb4 database_name = connection.current_database database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 1283b0642c..b8f51acba0 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -41,7 +41,7 @@ module ActiveRecord column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_24" } column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_25" } - # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 + # MySQL floats are precision 0..24, MySQL doubles are precision 25..53 assert_equal 24, column_no_limit.limit assert_equal 24, column_short.limit assert_equal 53, column_long.limit diff --git a/activerecord/test/cases/adapters/mysql2/set_test.rb b/activerecord/test/cases/adapters/mysql2/set_test.rb new file mode 100644 index 0000000000..89107e142f --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/set_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class Mysql2SetTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + class SetTest < ActiveRecord::Base + end + + def setup + SetTest.connection.create_table :set_tests, id: false, force: true do |t| + t.column :set_column, "set('text','blob','tiny','medium','long','unsigned','bigint')" + end + end + + def test_should_not_be_unsigned + column = SetTest.columns_hash["set_column"] + assert_not_predicate column, :unsigned? + end + + def test_should_not_be_bigint + column = SetTest.columns_hash["set_column"] + assert_not_predicate column, :bigint? + end + + def test_schema_dumping + schema = dump_table_schema "set_tests" + assert_match %r{t\.column "set_column", "set\('text','blob','tiny','medium','long','unsigned','bigint'\)"$}, schema + end +end diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb index 7b6dce71e9..626ef59570 100644 --- a/activerecord/test/cases/adapters/mysql2/sp_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb @@ -9,7 +9,7 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase def setup @connection = ActiveRecord::Base.connection - unless ActiveRecord::Base.connection.version >= "5.6.0" + unless ActiveRecord::Base.connection.database_version >= "5.6.0" skip("no stored procedure support") end end diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb index 1c92df940f..13cf1daa08 100644 --- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -73,7 +73,7 @@ class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase end end.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], ActiveRecord::Base.connection.schema_migration).migrate output = dump_table_schema("mysql_table_options") options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] @@ -112,7 +112,7 @@ class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase end end.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], ActiveRecord::Base.connection.schema_migration).migrate assert_match %r{ENGINE=InnoDB}, @log.string end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index 52e283f247..2041cc308f 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -92,7 +92,7 @@ module ActiveRecord test "raises StatementTimeout when statement timeout exceeded" do skip unless ActiveRecord::Base.connection.show_variable("max_execution_time") - assert_raises(ActiveRecord::StatementTimeout) do + error = assert_raises(ActiveRecord::StatementTimeout) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new latch2 = Concurrent::CountDownLatch.new @@ -117,10 +117,11 @@ module ActiveRecord thread.join end end + assert_kind_of ActiveRecord::QueryAborted, error end test "raises QueryCanceled when canceling statement due to user request" do - assert_raises(ActiveRecord::QueryCanceled) do + error = assert_raises(ActiveRecord::QueryCanceled) do s = Sample.create!(value: 1) latch = Concurrent::CountDownLatch.new @@ -144,6 +145,7 @@ module ActiveRecord thread.join end end + assert_kind_of ActiveRecord::QueryAborted, error end end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index afd422881b..62efaf3bfe 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -29,7 +29,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def test_add_index # add_index calls index_name_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false } + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.define_method(:index_name_exists?) { |*| false } expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') assert_equal expected, add_index(:people, :last_name, unique: true, where: "state = 'active'") @@ -74,12 +74,12 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase add_index(:people, :last_name, algorithm: :copy) end - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists? + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.remove_method :index_name_exists? end def test_remove_index # remove_index calls index_name_for_remove which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_for_remove) do |*| + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.define_method(:index_name_for_remove) do |*| "index_people_on_last_name" end @@ -90,7 +90,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase add_index(:people, :last_name, algorithm: :copy) end - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.remove_method :index_name_for_remove end def test_remove_index_when_name_is_specified diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 42618c2ec3..2e7a4b498f 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -17,7 +17,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase enable_extension!("hstore", @connection) @connection.transaction do - @connection.create_table("pg_arrays") do |t| + @connection.create_table "pg_arrays", force: true do |t| t.string "tags", array: true, limit: 255 t.integer "ratings", array: true t.datetime :datetimes, array: true @@ -112,6 +112,18 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_predicate column, :array? end + def test_change_column_from_non_array_to_array + @connection.add_column :pg_arrays, :snippets, :string + @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [], using: "string_to_array(\"snippets\", ',')" + + PgArray.reset_column_information + column = PgArray.columns_hash["snippets"] + + assert_equal :text, column.type + assert_equal [], PgArray.column_defaults["snippets"] + assert_predicate column, :array? + end + def test_change_column_cant_make_non_array_column_to_array @connection.add_column :pg_arrays, :a_string, :string assert_raises ActiveRecord::StatementInvalid do @@ -226,14 +238,6 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal(PgArray.last.tags, tag_values) end - def test_insert_fixtures - tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] - assert_deprecated do - @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") - end - assert_equal(PgArray.last.tags, tag_values) - end - def test_attribute_for_inspect_for_array_field record = PgArray.new { |a| a.ratings = (1..10).to_a } assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings)) diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 3988c2adca..531e6b2328 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -35,7 +35,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase def test_binary_columns_are_limitless_the_upper_limit_is_one_GB assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000) - assert_raise ActiveRecord::ActiveRecordError do + assert_raise ArgumentError do @connection.type_to_sql(:binary, limit: 4294967295) end end diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb index 305e033642..79e9efcf06 100644 --- a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb +++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb @@ -7,22 +7,21 @@ class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase def test_case_insensitiveness connection = ActiveRecord::Base.connection - table = Default.arel_table - column = Default.columns_hash["char1"] - comparison = connection.case_insensitive_comparison table, :char1, column, nil + attr = Default.arel_attribute(:char1) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) - column = Default.columns_hash["char2"] - comparison = connection.case_insensitive_comparison table, :char2, column, nil + attr = Default.arel_attribute(:char2) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) - column = Default.columns_hash["char3"] - comparison = connection.case_insensitive_comparison table, :char3, column, nil + attr = Default.arel_attribute(:char3) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) - column = Default.columns_hash["multiline_default"] - comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil + attr = Default.arel_attribute(:multiline_default) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) end end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index b0ce2694a3..683066cdb3 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -15,13 +15,13 @@ module PostgresqlCompositeBehavior @connection = ActiveRecord::Base.connection @connection.transaction do - @connection.execute <<-SQL - CREATE TYPE full_address AS - ( - city VARCHAR(90), - street VARCHAR(90) - ); - SQL + @connection.execute <<~SQL + CREATE TYPE full_address AS + ( + city VARCHAR(90), + street VARCHAR(90) + ); + SQL @connection.create_table("postgresql_composites") do |t| t.column :address, :full_address end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 70aa189893..dcee4fd22d 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -25,28 +25,20 @@ module ActiveRecord super end - def test_truncate - count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i - assert_operator count, :>, 0 - ActiveRecord::Base.connection.truncate("comments") - count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i - assert_equal 0, count - end - def test_encoding - assert_queries(1) do + assert_queries(1, ignore_none: true) do assert_not_nil @connection.encoding end end def test_collation - assert_queries(1) do + assert_queries(1, ignore_none: true) do assert_not_nil @connection.collation end end def test_ctype - assert_queries(1) do + assert_queries(1, ignore_none: true) do assert_not_nil @connection.ctype end end @@ -146,34 +138,15 @@ module ActiveRecord end end - # Must have PostgreSQL >= 9.2, or with_manual_interventions set to - # true for this test to run. - # - # When prompted, restart the PostgreSQL server with the - # "-m fast" option or kill the individual connection assuming - # you know the incantation to do that. - # To restart PostgreSQL 9.1 on macOS, installed via MacPorts, ... - # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" def test_reconnection_after_actual_disconnection_with_verify original_connection_pid = @connection.query("select pg_backend_pid()") # Sanity check. assert_predicate @connection, :active? - if @connection.send(:postgresql_version) >= 90200 - secondary_connection = ActiveRecord::Base.connection_pool.checkout - secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") - ActiveRecord::Base.connection_pool.checkin(secondary_connection) - elsif ARTest.config["with_manual_interventions"] - puts "Kill the connection now (e.g. by restarting the PostgreSQL " \ - 'server with the "-m fast" option) and then press enter.' - $stdin.gets - else - # We're not capable of terminating the backend ourselves, and - # we're not allowed to seek assistance; bail out without - # actually testing anything. - return - end + secondary_connection = ActiveRecord::Base.connection_pool.checkout + secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") + ActiveRecord::Base.connection_pool.checkin(secondary_connection) @connection.verify! @@ -230,7 +203,7 @@ module ActiveRecord def test_get_and_release_advisory_lock lock_id = 5295901941911233559 - list_advisory_locks = <<-SQL + list_advisory_locks = <<~SQL SELECT locktype, (classid::bigint << 32) | objid::bigint AS lock_id FROM pg_locks @@ -261,8 +234,11 @@ module ActiveRecord end end - private + def test_supports_ranges_is_deprecated + assert_deprecated { @connection.supports_ranges? } + end + private def with_warning_suppression log_level = @connection.client_min_messages @connection.client_min_messages = "error" diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index b7535d5c9a..562cf1f2d1 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -64,7 +64,7 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase def test_text_columns_are_limitless_the_upper_limit_is_one_GB assert_equal "text", @connection.type_to_sql(:text, limit: 100_000) - assert_raise ActiveRecord::ActiveRecordError do + assert_raise ArgumentError do @connection.type_to_sql(:text, limit: 4294967295) end end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 6789ff63e7..416a2b141b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -13,7 +13,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase def setup @connection = ActiveRecord::Base.connection @connection.transaction do - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); SQL @connection.create_table("postgresql_enums") do |t| diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index df97ab11e7..16baa8933d 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -22,23 +22,26 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase @connection = ActiveRecord::Base.connection - @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name @old_table_name_prefix = ActiveRecord::Base.table_name_prefix @old_table_name_suffix = ActiveRecord::Base.table_name_suffix ActiveRecord::Base.table_name_prefix = "p_" ActiveRecord::Base.table_name_suffix = "_s" - ActiveRecord::SchemaMigration.delete_all rescue nil - ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s" + @connection.schema_migration.reset_table_name + ActiveRecord::InternalMetadata.reset_table_name + + @connection.schema_migration.delete_all rescue nil ActiveRecord::Migration.verbose = false end def teardown + @connection.schema_migration.delete_all rescue nil + ActiveRecord::Migration.verbose = true + ActiveRecord::Base.table_name_prefix = @old_table_name_prefix ActiveRecord::Base.table_name_suffix = @old_table_name_suffix - ActiveRecord::SchemaMigration.delete_all rescue nil - ActiveRecord::Migration.verbose = true - ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name + @connection.schema_migration.reset_table_name + ActiveRecord::InternalMetadata.reset_table_name super end @@ -47,7 +50,7 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase @connection.disable_extension("hstore") migrations = [EnableHstore.new(nil, 1)] - ActiveRecord::Migrator.new(:up, migrations).migrate + ActiveRecord::Migrator.new(:up, migrations, ActiveRecord::Base.connection.schema_migration).migrate assert @connection.extension_enabled?("hstore"), "extension hstore should be enabled" end @@ -55,7 +58,7 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase @connection.enable_extension("hstore") migrations = [DisableHstore.new(nil, 1)] - ActiveRecord::Migrator.new(:up, migrations).migrate + ActiveRecord::Migrator.new(:up, migrations, ActiveRecord::Base.connection.schema_migration).migrate assert_not @connection.extension_enabled?("hstore"), "extension hstore should not be enabled" end end diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb index 4fa315ad23..69339c8a31 100644 --- a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb @@ -22,18 +22,18 @@ if ActiveRecord::Base.connection.supports_foreign_tables? enable_extension!("postgres_fdw", @connection) foreign_db_config = ARTest.connection_config["arunit2"] - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE SERVER foreign_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (dbname '#{foreign_db_config["database"]}') SQL - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE USER MAPPING FOR CURRENT_USER SERVER foreign_server SQL - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE FOREIGN TABLE foreign_professors ( id int, name character varying NOT NULL @@ -45,7 +45,7 @@ if ActiveRecord::Base.connection.supports_foreign_tables? def teardown disable_extension!("postgres_fdw", @connection) - @connection.execute <<-SQL + @connection.execute <<~SQL DROP SERVER IF EXISTS foreign_server CASCADE SQL end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 8c6f046553..f312b6e23d 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -247,7 +247,7 @@ class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase class PostgresqlLine < ActiveRecord::Base; end setup do - unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400 + unless ActiveRecord::Base.connection.database_version >= 90400 skip("line type is not fully implemented") end @connection = ActiveRecord::Base.connection @@ -361,7 +361,6 @@ class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase end private - def assert_column_exists(column_name) assert connection.column_exists?(table_name, column_name) end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 4b061a9375..671d8211a7 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "support/schema_dumping_helper" +require "support/stubs/strong_parameters" class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper @@ -11,12 +12,6 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase store_accessor :settings, :language, :timezone end - class FakeParameters - def to_unsafe_h - { "hi" => "hi" } - end - end - def setup @connection = ActiveRecord::Base.connection @@ -158,6 +153,22 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase assert_equal "GMT", y.timezone end + def test_changes_with_store_accessors + x = Hstore.new(language: "de") + assert x.language_changed? + assert_nil x.language_was + assert_equal [nil, "de"], x.language_change + x.save! + + assert_not x.language_changed? + x.reload + + x.settings = nil + assert x.language_changed? + assert_equal "de", x.language_was + assert_equal ["de", nil], x.language_change + end + def test_changes_in_place hstore = Hstore.create!(settings: { "one" => "two" }) hstore.settings["three"] = "four" @@ -344,7 +355,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase end def test_supports_to_unsafe_h_values - assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new)) + assert_equal "\"hi\"=>\"hi\"", @type.serialize(ProtectedParams.new("hi" => "hi")) end private diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 5e56ce8427..b1bf06d9e9 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -71,17 +71,15 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase end test "assigning 'infinity' on a datetime column with TZ aware attributes" do - begin - in_time_zone "Pacific Time (US & Canada)" do - record = PostgresqlInfinity.create!(datetime: "infinity") - assert_equal Float::INFINITY, record.datetime - assert_equal record.datetime, record.reload.datetime - end - ensure - # setting time_zone_aware_attributes causes the types to change. - # There is no way to do this automatically since it can be set on a superclass - PostgresqlInfinity.reset_column_information + in_time_zone "Pacific Time (US & Canada)" do + record = PostgresqlInfinity.create!(datetime: "infinity") + assert_equal Float::INFINITY, record.datetime + assert_equal record.datetime, record.reload.datetime end + ensure + # setting time_zone_aware_attributes causes the types to change. + # There is no way to do this automatically since it can be set on a superclass + PostgresqlInfinity.reset_column_information end test "where clause with infinite range on a datetime column" do diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 1aa0348879..ff2ab22a80 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -54,8 +54,12 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase type = PostgresqlMoney.type_for_attribute("wealth") assert_equal(12345678.12, type.cast(+"$12,345,678.12")) assert_equal(12345678.12, type.cast(+"$12.345.678,12")) + assert_equal(12345678.12, type.cast(+"12,345,678.12")) + assert_equal(12345678.12, type.cast(+"12.345.678,12")) assert_equal(-1.15, type.cast(+"-$1.15")) assert_equal(-2.25, type.cast(+"($2.25)")) + assert_equal(-1.15, type.cast(+"-1.15")) + assert_equal(-2.25, type.cast(+"(2.25)")) end def test_schema_dumping diff --git a/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb new file mode 100644 index 0000000000..5b9f5e0832 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +if supports_optimizer_hints? + class PostgresqlOptimzerHintsTest < ActiveRecord::PostgreSQLTestCase + fixtures :posts + + def setup + enable_extension!("pg_hint_plan", ActiveRecord::Base.connection) + end + + def test_optimizer_hints + assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do + posts = Post.optimizer_hints("SeqScan(posts)") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_includes posts.explain, "Seq Scan on posts" + end + end + + def test_optimizer_hints_with_count_subquery + assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do + posts = Post.optimizer_hints("SeqScan(posts)") + posts = posts.select(:id).where(author_id: [0, 1]).limit(5) + assert_equal 5, posts.count + end + end + + def test_optimizer_hints_is_sanitized + assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do + posts = Post.optimizer_hints("/*+ SeqScan(posts) */") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_includes posts.explain, "Seq Scan on posts" + end + + assert_sql(%r{\ASELECT /\*\+ "posts"\.\*, \*/}) do + posts = Post.optimizer_hints("**// \"posts\".*, //**") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_equal({ "id" => 1 }, posts.first.as_json) + end + end + + def test_optimizer_hints_with_unscope + assert_sql(%r{\ASELECT "posts"\."id"}) do + posts = Post.optimizer_hints("/*+ SeqScan(posts) */") + posts = posts.select(:id).where(author_id: [0, 1]) + posts.unscope(:optimizer_hints).load + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/partitions_test.rb b/activerecord/test/cases/adapters/postgresql/partitions_test.rb index 0ac9ca1200..4015bc94f9 100644 --- a/activerecord/test/cases/adapters/postgresql/partitions_test.rb +++ b/activerecord/test/cases/adapters/postgresql/partitions_test.rb @@ -12,7 +12,7 @@ class PostgreSQLPartitionsTest < ActiveRecord::PostgreSQLTestCase end def test_partitions_table_exists - skip unless ActiveRecord::Base.connection.postgresql_version >= 100000 + skip unless ActiveRecord::Base.connection.database_version >= 100000 @connection.create_table :partitioned_events, force: true, id: false, options: "partition by range (issued_at)" do |t| t.timestamp :issued_at diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index cbb6cd42b5..d99593817a 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -13,6 +13,7 @@ module ActiveRecord def setup @connection = ActiveRecord::Base.connection + @connection_handler = ActiveRecord::Base.connection_handler end def test_bad_connection @@ -23,6 +24,18 @@ module ActiveRecord end end + def test_database_exists_returns_false_when_the_database_does_not_exist + config = { database: "non_extant_database", adapter: "postgresql" } + assert_not ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.database_exists?(config), + "expected database #{config[:database]} to not exist" + end + + def test_database_exists_returns_true_when_the_database_exists + config = ActiveRecord::Base.configurations["arunit"] + assert ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.database_exists?(config), + "expected database #{config[:database]} to exist" + end + def test_primary_key with_example_table do assert_equal "id", @connection.primary_key("ex") @@ -376,8 +389,73 @@ module ActiveRecord end end - private + def test_errors_when_an_insert_query_is_called_while_preventing_writes + with_example_table do + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + end + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @connection.execute("UPDATE ex SET data = '9989' WHERE data = '138853948594'") + end + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @connection.execute("DELETE FROM ex where data = '138853948594'") + end + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @connection_handler.while_preventing_writes do + assert_equal 1, @connection.execute("SELECT * FROM ex WHERE data = '138853948594'").entries.count + end + end + end + + def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes + @connection_handler.while_preventing_writes do + assert_equal 1, @connection.execute("SHOW TIME ZONE").entries.count + end + end + def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes + @connection_handler.while_preventing_writes do + assert_equal [], @connection.execute("SET standard_conforming_strings = on").entries + end + end + + def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @connection_handler.while_preventing_writes do + assert_equal 1, @connection.execute("(\n( SELECT * FROM ex WHERE data = '138853948594' ) )").entries.count + end + end + end + + private def with_example_table(definition = "id serial primary key, number integer, data character varying(255)", &block) super(@connection, "ex", definition, &block) end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index d50dc49276..d571355a9c 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -39,6 +39,11 @@ module ActiveRecord type = OID::Bit.new assert_nil @conn.quote(type.serialize(value)) end + + def test_quote_table_name_with_spaces + value = "user posts" + assert_equal "\"user posts\"", @conn.quote_table_name(value) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 433598500d..068f1e8bea 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -3,418 +3,432 @@ require "cases/helper" require "support/connection_helper" -if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges? - class PostgresqlRange < ActiveRecord::Base - self.table_name = "postgresql_ranges" - self.time_zone_aware_types += [:tsrange, :tstzrange] - end - - class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase - self.use_transactional_tests = false - include ConnectionHelper - include InTimeZone - - def setup - @connection = PostgresqlRange.connection - begin - @connection.transaction do - @connection.execute <<_SQL - CREATE TYPE floatrange AS RANGE ( - subtype = float8, - subtype_diff = float8mi - ); -_SQL - - @connection.create_table("postgresql_ranges") do |t| - t.daterange :date_range - t.numrange :num_range - t.tsrange :ts_range - t.tstzrange :tstz_range - t.int4range :int4_range - t.int8range :int8_range - end - - @connection.add_column "postgresql_ranges", "float_range", "floatrange" +class PostgresqlRange < ActiveRecord::Base + self.table_name = "postgresql_ranges" + self.time_zone_aware_types += [:tsrange, :tstzrange] +end + +class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + include ConnectionHelper + include InTimeZone + + def setup + @connection = PostgresqlRange.connection + begin + @connection.transaction do + @connection.execute <<~SQL + CREATE TYPE floatrange AS RANGE ( + subtype = float8, + subtype_diff = float8mi + ); + SQL + + @connection.create_table("postgresql_ranges") do |t| + t.daterange :date_range + t.numrange :num_range + t.tsrange :ts_range + t.tstzrange :tstz_range + t.int4range :int4_range + t.int8range :int8_range end - PostgresqlRange.reset_column_information - rescue ActiveRecord::StatementInvalid - skip "do not test on PG without range" - end - insert_range(id: 101, - date_range: "[''2012-01-02'', ''2012-01-04'']", - num_range: "[0.1, 0.2]", - ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", - int4_range: "[1, 10]", - int8_range: "[10, 100]", - float_range: "[0.5, 0.7]") - - insert_range(id: 102, - date_range: "[''2012-01-02'', ''2012-01-04'')", - num_range: "[0.1, 0.2)", - ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", - int4_range: "[1, 10)", - int8_range: "[10, 100)", - float_range: "[0.5, 0.7)") - - insert_range(id: 103, - date_range: "[''2012-01-02'',]", - num_range: "[0.1,]", - ts_range: "[''2010-01-01 14:30'',]", - tstz_range: "[''2010-01-01 14:30:00+05'',]", - int4_range: "[1,]", - int8_range: "[10,]", - float_range: "[0.5,]") - - insert_range(id: 104, - date_range: "[,]", - num_range: "[,]", - ts_range: "[,]", - tstz_range: "[,]", - int4_range: "[,]", - int8_range: "[,]", - float_range: "[,]") - - insert_range(id: 105, - date_range: "[''2012-01-02'', ''2012-01-02'')", - num_range: "[0.1, 0.1)", - ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", - int4_range: "[1, 1)", - int8_range: "[10, 10)", - float_range: "[0.5, 0.5)") - - @new_range = PostgresqlRange.new - @first_range = PostgresqlRange.find(101) - @second_range = PostgresqlRange.find(102) - @third_range = PostgresqlRange.find(103) - @fourth_range = PostgresqlRange.find(104) - @empty_range = PostgresqlRange.find(105) - end + @connection.add_column "postgresql_ranges", "float_range", "floatrange" + end + PostgresqlRange.reset_column_information + rescue ActiveRecord::StatementInvalid + skip "do not test on PG without range" + end + + insert_range(id: 101, + date_range: "[''2012-01-02'', ''2012-01-04'']", + num_range: "[0.1, 0.2]", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", + int4_range: "[1, 10]", + int8_range: "[10, 100]", + float_range: "[0.5, 0.7]") + + insert_range(id: 102, + date_range: "[''2012-01-02'', ''2012-01-04'')", + num_range: "[0.1, 0.2)", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", + int4_range: "[1, 10)", + int8_range: "[10, 100)", + float_range: "[0.5, 0.7)") + + insert_range(id: 103, + date_range: "[''2012-01-02'',]", + num_range: "[0.1,]", + ts_range: "[''2010-01-01 14:30'',]", + tstz_range: "[''2010-01-01 14:30:00+05'',]", + int4_range: "[1,]", + int8_range: "[10,]", + float_range: "[0.5,]") + + insert_range(id: 104, + date_range: "[,]", + num_range: "[,]", + ts_range: "[,]", + tstz_range: "[,]", + int4_range: "[,]", + int8_range: "[,]", + float_range: "[,]") + + insert_range(id: 105, + date_range: "[''2012-01-02'', ''2012-01-02'')", + num_range: "[0.1, 0.1)", + ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", + int4_range: "[1, 1)", + int8_range: "[10, 10)", + float_range: "[0.5, 0.5)") + + @new_range = PostgresqlRange.new + @first_range = PostgresqlRange.find(101) + @second_range = PostgresqlRange.find(102) + @third_range = PostgresqlRange.find(103) + @fourth_range = PostgresqlRange.find(104) + @empty_range = PostgresqlRange.find(105) + end - teardown do - @connection.drop_table "postgresql_ranges", if_exists: true - @connection.execute "DROP TYPE IF EXISTS floatrange" - reset_connection - end + teardown do + @connection.drop_table "postgresql_ranges", if_exists: true + @connection.execute "DROP TYPE IF EXISTS floatrange" + reset_connection + end - def test_data_type_of_range_types - assert_equal :daterange, @first_range.column_for_attribute(:date_range).type - assert_equal :numrange, @first_range.column_for_attribute(:num_range).type - assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type - assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type - assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type - assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type - end + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end - def test_int4range_values - assert_equal 1...11, @first_range.int4_range - assert_equal 1...10, @second_range.int4_range - assert_equal 1...Float::INFINITY, @third_range.int4_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_nil @empty_range.int4_range - end + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 1...10, @second_range.int4_range + assert_equal 1...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end - def test_int8range_values - assert_equal 10...101, @first_range.int8_range - assert_equal 10...100, @second_range.int8_range - assert_equal 10...Float::INFINITY, @third_range.int8_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_nil @empty_range.int8_range - end + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 10...100, @second_range.int8_range + assert_equal 10...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end - def test_daterange_values - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_nil @empty_range.date_range - end + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end - def test_numrange_values - assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range - assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range - assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range - assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range - assert_nil @empty_range.num_range - end + def test_numrange_values + assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range + assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range + assert_nil @empty_range.num_range + end - def test_tsrange_values - tz = ::ActiveRecord::Base.default_timezone - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_nil @empty_range.ts_range - end + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end - def test_tstzrange_values - assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range - assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_nil @empty_range.tstz_range - end + def test_tstzrange_values + assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range + assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end - def test_custom_range_values - assert_equal 0.5..0.7, @first_range.float_range - assert_equal 0.5...0.7, @second_range.float_range - assert_equal 0.5...Float::INFINITY, @third_range.float_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) - assert_nil @empty_range.float_range - end + def test_custom_range_values + assert_equal 0.5..0.7, @first_range.float_range + assert_equal 0.5...0.7, @second_range.float_range + assert_equal 0.5...Float::INFINITY, @third_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) + assert_nil @empty_range.float_range + end - def test_timezone_awareness_tzrange - tz = "Pacific Time (US & Canada)" + def test_timezone_awareness_tzrange + tz = "Pacific Time (US & Canada)" - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = Time.current.to_s - time = Time.zone.parse(time_string) + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) - record = PostgresqlRange.new(tstz_range: time_string..time_string) - assert_equal time..time, record.tstz_range - assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + record = PostgresqlRange.new(tstz_range: time_string..time_string) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone - record.save! - record.reload + record.save! + record.reload - assert_equal time..time, record.tstz_range - assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone - end + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone end + end - def test_create_tstzrange - tstzrange = Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2011-02-02 14:30:00 CDT") - round_trip(@new_range, :tstz_range, tstzrange) - assert_equal @new_range.tstz_range, tstzrange - assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC") - end + def test_create_tstzrange + tstzrange = Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2011-02-02 14:30:00 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC") + end - def test_update_tstzrange - assert_equal_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET")) - assert_nil_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000")) - end + def test_update_tstzrange + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000")) + end - def test_create_tsrange - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@new_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) - end + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + end - def test_update_tsrange - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) - assert_nil_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) - end + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) + end - def test_timezone_awareness_tsrange - tz = "Pacific Time (US & Canada)" + def test_timezone_awareness_tsrange + tz = "Pacific Time (US & Canada)" - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = Time.current.to_s - time = Time.zone.parse(time_string) + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) - record = PostgresqlRange.new(ts_range: time_string..time_string) - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - record.save! - record.reload + record.save! + record.reload - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - end + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone end + end - def test_create_tstzrange_preserve_usec - tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") - round_trip(@new_range, :tstz_range, tstzrange) - assert_equal @new_range.tstz_range, tstzrange - assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") - end + def test_create_tstzrange_preserve_usec + tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") + end - def test_update_tstzrange_preserve_usec - assert_equal_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) - assert_nil_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) - end + def test_update_tstzrange_preserve_usec + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) + end - def test_create_tsrange_preseve_usec - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@new_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) - end + def test_create_tsrange_preseve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + end - def test_update_tsrange_preserve_usec - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) - assert_nil_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) - end + def test_update_tsrange_preserve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) + end - def test_timezone_awareness_tsrange_preserve_usec - tz = "Pacific Time (US & Canada)" + def test_timezone_awareness_tsrange_preserve_usec + tz = "Pacific Time (US & Canada)" - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = "2017-09-26 07:30:59.132451 -0700" - time = Time.zone.parse(time_string) - assert time.usec > 0 + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = "2017-09-26 07:30:59.132451 -0700" + time = Time.zone.parse(time_string) + assert time.usec > 0 - record = PostgresqlRange.new(ts_range: time_string..time_string) - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - assert_equal time.usec, record.ts_range.begin.usec + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec - record.save! - record.reload + record.save! + record.reload - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - assert_equal time.usec, record.ts_range.begin.usec - end + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec end + end - def test_create_numrange - assert_equal_round_trip(@new_range, :num_range, - BigDecimal("0.5")...BigDecimal("1")) - end + def test_create_numrange + assert_equal_round_trip(@new_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + end - def test_update_numrange - assert_equal_round_trip(@first_range, :num_range, - BigDecimal("0.5")...BigDecimal("1")) - assert_nil_round_trip(@first_range, :num_range, - BigDecimal("0.5")...BigDecimal("0.5")) - end + def test_update_numrange + assert_equal_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + assert_nil_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("0.5")) + end - def test_create_daterange - assert_equal_round_trip(@new_range, :date_range, - Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) - end + def test_create_daterange + assert_equal_round_trip(@new_range, :date_range, + Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + end - def test_update_daterange - assert_equal_round_trip(@first_range, :date_range, - Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) - assert_nil_round_trip(@first_range, :date_range, - Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) - end + def test_update_daterange + assert_equal_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) + assert_nil_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) + end - def test_create_int4range - assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) - end + def test_create_int4range + assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) + end - def test_update_int4range - assert_equal_round_trip(@first_range, :int4_range, 6...10) - assert_nil_round_trip(@first_range, :int4_range, 3...3) - end + def test_update_int4range + assert_equal_round_trip(@first_range, :int4_range, 6...10) + assert_nil_round_trip(@first_range, :int4_range, 3...3) + end - def test_create_int8range - assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) - end + def test_create_int8range + assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) + end - def test_update_int8range - assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) - assert_nil_round_trip(@first_range, :int8_range, 39999...39999) - end + def test_update_int8range + assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) + assert_nil_round_trip(@first_range, :int8_range, 39999...39999) + end - def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported - assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } - assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } - assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } - end + def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported + assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } + end - def test_where_by_attribute_with_range - range = 1..100 - record = PostgresqlRange.create!(int4_range: range) - assert_equal record, PostgresqlRange.where(int4_range: range).take - end + def test_where_by_attribute_with_range + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: range).take + end - def test_where_by_attribute_with_range_in_array - range = 1..100 - record = PostgresqlRange.create!(int4_range: range) - assert_equal record, PostgresqlRange.where(int4_range: [range]).take - end + def test_where_by_attribute_with_range_in_array + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: [range]).take + end - def test_update_all_with_ranges - PostgresqlRange.create! + def test_update_all_with_ranges + PostgresqlRange.create! - PostgresqlRange.update_all(int8_range: 1..100) + PostgresqlRange.update_all(int8_range: 1..100) - assert_equal 1...101, PostgresqlRange.first.int8_range - end + assert_equal 1...101, PostgresqlRange.first.int8_range + end - def test_ranges_correctly_escape_input - range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" - PostgresqlRange.update_all(int8_range: range) + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) - assert_nothing_raised do - PostgresqlRange.first - end + assert_nothing_raised do + PostgresqlRange.first end + end - def test_infinity_values - PostgresqlRange.create!(int4_range: 1..Float::INFINITY, - int8_range: -Float::INFINITY..0, - float_range: -Float::INFINITY..Float::INFINITY) + def test_infinity_values + PostgresqlRange.create!(int4_range: 1..Float::INFINITY, + int8_range: -Float::INFINITY..0, + float_range: -Float::INFINITY..Float::INFINITY) - record = PostgresqlRange.first + record = PostgresqlRange.first - assert_equal(1...Float::INFINITY, record.int4_range) - assert_equal(-Float::INFINITY...1, record.int8_range) - assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) - end + assert_equal(1...Float::INFINITY, record.int4_range) + assert_equal(-Float::INFINITY...1, record.int8_range) + assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) + end - private - def assert_equal_round_trip(range, attribute, value) - round_trip(range, attribute, value) - assert_equal value, range.public_send(attribute) - end + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0") + def test_endless_range_values + record = PostgresqlRange.create!( + int4_range: eval("1.."), + int8_range: eval("10.."), + float_range: eval("0.5..") + ) - def assert_nil_round_trip(range, attribute, value) - round_trip(range, attribute, value) - assert_nil range.public_send(attribute) - end - - def round_trip(range, attribute, value) - range.public_send "#{attribute}=", value - assert range.save - assert range.reload - end + record = PostgresqlRange.find(record.id) - def insert_range(values) - @connection.execute <<-SQL - INSERT INTO postgresql_ranges ( - id, - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range, - float_range - ) VALUES ( - #{values[:id]}, - '#{values[:date_range]}', - '#{values[:num_range]}', - '#{values[:ts_range]}', - '#{values[:tstz_range]}', - '#{values[:int4_range]}', - '#{values[:int8_range]}', - '#{values[:float_range]}' - ) - SQL - end + assert_equal 1...Float::INFINITY, record.int4_range + assert_equal 10...Float::INFINITY, record.int8_range + assert_equal 0.5...Float::INFINITY, record.float_range + end end + + private + def assert_equal_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_equal value, range.public_send(attribute) + end + + def assert_nil_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_nil range.public_send(attribute) + end + + def round_trip(range, attribute, value) + range.public_send "#{attribute}=", value + assert range.save + assert range.reload + end + + def insert_range(values) + @connection.execute <<~SQL + INSERT INTO postgresql_ranges ( + id, + date_range, + num_range, + ts_range, + tstz_range, + int4_range, + int8_range, + float_range + ) VALUES ( + #{values[:id]}, + '#{values[:date_range]}', + '#{values[:num_range]}', + '#{values[:ts_range]}', + '#{values[:tstz_range]}', + '#{values[:int4_range]}', + '#{values[:int8_range]}', + '#{values[:float_range]}' + ) + SQL + end end diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index ba477c63f4..a4f722c063 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -13,7 +13,7 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase end module MissingSuperuserPrivileges - def execute(sql) + def execute(sql, name = nil) if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) super "BROKEN;" rescue nil # put transaction in broken state raise ActiveRecord::StatementInvalid, "PG::InsufficientPrivilege" @@ -24,7 +24,7 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase end module ProgrammerMistake - def execute(sql) + def execute(sql, name = nil) if IS_REFERENTIAL_INTEGRITY_SQL.call(sql) raise ArgumentError, "something is not right." else @@ -106,7 +106,6 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase end private - def assert_transaction_is_not_broken assert_equal 1, @connection.select_value("SELECT 1") end diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index 100d247113..fae20de086 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -25,9 +25,8 @@ class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase end private - def num_indices_named(name) - @connection.execute(<<-SQL).values.length + @connection.execute(<<~SQL).values.length SELECT 1 FROM "pg_index" JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" WHERE "pg_class"."relname" = '#{name}' diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index a36d066c80..fe6a3deff4 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -104,27 +104,27 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase end def test_schema_names - assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names + schema_names = @connection.schema_names + assert_includes schema_names, "public" + assert_includes schema_names, "test_schema" + assert_includes schema_names, "test_schema2" + assert_includes schema_names, "hint_plan" if @connection.supports_optimizer_hints? end def test_create_schema - begin - @connection.create_schema "test_schema3" - assert @connection.schema_names.include? "test_schema3" - ensure - @connection.drop_schema "test_schema3" - end + @connection.create_schema "test_schema3" + assert @connection.schema_names.include? "test_schema3" + ensure + @connection.drop_schema "test_schema3" end def test_raise_create_schema_with_existing_schema - begin + @connection.create_schema "test_schema3" + assert_raises(ActiveRecord::StatementInvalid) do @connection.create_schema "test_schema3" - assert_raises(ActiveRecord::StatementInvalid) do - @connection.create_schema "test_schema3" - end - ensure - @connection.drop_schema "test_schema3" end + ensure + @connection.drop_schema "test_schema3" end def test_drop_schema @@ -146,7 +146,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_habtm_table_name_with_schema ActiveRecord::Base.connection.drop_schema "music", if_exists: true ActiveRecord::Base.connection.create_schema "music" - ActiveRecord::Base.connection.execute <<-SQL + ActiveRecord::Base.connection.execute <<~SQL CREATE TABLE music.albums (id serial primary key); CREATE TABLE music.songs (id serial primary key); CREATE TABLE music.albums_songs (album_id integer, song_id integer); @@ -507,6 +507,7 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase @connection = ActiveRecord::Base.connection @connection.create_table "trains" do |t| t.string :name + t.string :position t.text :description end end @@ -530,6 +531,17 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase assert_match(/opclass: \{ description: :text_pattern_ops \}/, output) end + + def test_opclass_class_parsing_on_non_reserved_and_cannot_be_function_or_type_keyword + @connection.enable_extension("pg_trgm") + @connection.execute "CREATE INDEX trains_position ON trains USING gin(position gin_trgm_ops)" + @connection.execute "CREATE INDEX trains_name_and_position ON trains USING btree(name, position text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: :gin_trgm_ops/, output) + assert_match(/opclass: \{ position: :text_pattern_ops \}/, output) + end end class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index 984b2f5ea4..311863a418 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -94,7 +94,6 @@ module ActiveRecord end test "raises LockWaitTimeout when lock wait timeout exceeded" do - skip unless ActiveRecord::Base.connection.postgresql_version >= 90300 assert_raises(ActiveRecord::LockWaitTimeout) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new @@ -178,7 +177,6 @@ module ActiveRecord end private - def with_warning_suppression log_level = ActiveRecord::Base.connection.client_min_messages ActiveRecord::Base.connection.client_min_messages = "error" diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 71d07e2f4c..a1c985fc71 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -114,6 +114,22 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase assert_equal "foobar", uuid.guid_before_type_cast end + def test_invalid_uuid_dont_match_to_nil + UUIDType.create! + assert_empty UUIDType.where(guid: "") + assert_empty UUIDType.where(guid: "foobar") + end + + class DuckUUID + def initialize(uuid) + @uuid = uuid + end + + def to_s + @uuid + end + end + def test_acceptable_uuid_regex # Valid uuids ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", @@ -125,9 +141,11 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.) "{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}", + # Support Object-Oriented UUIDs which respond to #to_s + DuckUUID.new("A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"), ].each do |valid_uuid| uuid = UUIDType.new guid: valid_uuid - assert_not_nil uuid.guid + assert_instance_of String, uuid.guid end # Invalid uuids @@ -198,10 +216,10 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase # Create custom PostgreSQL function to generate UUIDs # to test dumping tables which columns have defaults with custom functions - connection.execute <<-SQL - CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid - AS $$ SELECT * FROM #{uuid_function} $$ - LANGUAGE SQL VOLATILE; + connection.execute <<~SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM #{uuid_function} $$ + LANGUAGE SQL VOLATILE; SQL # Create such a table with custom function as default value generator @@ -275,14 +293,16 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase create_table("pg_uuids_4", id: :uuid) end end.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], ActiveRecord::Base.connection.schema_migration).migrate schema = dump_table_schema "pg_uuids_4" assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) ensure drop_table "pg_uuids_4" ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::Base.connection.schema_migration.delete_all end + uses_transaction :test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration end class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase @@ -323,14 +343,16 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase create_table("pg_uuids_4", id: :uuid, default: nil) end end.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], ActiveRecord::Base.connection.schema_migration).migrate schema = dump_table_schema "pg_uuids_4" assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema) ensure drop_table "pg_uuids_4" ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::Base.connection.schema_migration.delete_all end + uses_transaction :test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration end class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb deleted file mode 100644 index 93a7dafebd..0000000000 --- a/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require "cases/helper" -require "models/topic" - -module ActiveRecord - module ConnectionAdapters - class SQLite3Adapter - class BindParameterTest < ActiveRecord::SQLite3TestCase - def test_too_many_binds - topics = Topic.where(id: (1..999).to_a << 2**63) - assert_equal Topic.count, topics.count - - topics = Topic.where.not(id: (1..999).to_a << 2**63) - assert_equal 0, topics.count - end - end - end - end -end diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb index 76c8f7d8dd..d938b5ff2f 100644 --- a/activerecord/test/cases/adapters/sqlite3/collation_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -11,6 +11,10 @@ class SQLite3CollationTest < ActiveRecord::SQLite3TestCase @connection.create_table :collation_table_sqlite3, force: true do |t| t.string :string_nocase, collation: "NOCASE" t.text :text_rtrim, collation: "RTRIM" + # The decimal column might interfere with collation parsing. + # Thus, add this column type and some other string column afterwards. + t.decimal :decimal_col, precision: 6, scale: 2 + t.string :string_after_decimal_nocase, collation: "NOCASE" end end @@ -22,6 +26,11 @@ class SQLite3CollationTest < ActiveRecord::SQLite3TestCase column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == "string_nocase" } assert_equal :string, column.type assert_equal "NOCASE", column.collation + + # Verify collation of a column behind the decimal column as well. + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == "string_after_decimal_nocase" } + assert_equal :string, column.type + assert_equal "NOCASE", column.collation end test "text column with collation" do diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 40b58e86bf..9d26f32102 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -6,12 +6,8 @@ require "securerandom" class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase def setup + super @conn = ActiveRecord::Base.connection - @initial_represent_boolean_as_integer = ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer - end - - def teardown - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = @initial_represent_boolean_as_integer end def test_type_cast_binary_encoding_without_logger @@ -22,18 +18,10 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase end def test_type_cast_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false - assert_equal "t", @conn.type_cast(true) - - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true assert_equal 1, @conn.type_cast(true) end def test_type_cast_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false - assert_equal "f", @conn.type_cast(false) - - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true assert_equal 0, @conn.type_cast(false) end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 89052019f8..b6d72c7bcd 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -19,6 +19,8 @@ module ActiveRecord @conn = Base.sqlite3_connection database: ":memory:", adapter: "sqlite3", timeout: 100 + + @connection_handler = ActiveRecord::Base.connection_handler end def test_bad_connection @@ -28,6 +30,17 @@ module ActiveRecord end end + def test_database_exists_returns_false_when_the_database_does_not_exist + assert_not SQLite3Adapter.database_exists?(adapter: "sqlite3", database: "non_extant_db"), + "expected non_extant_db to not exist" + end + + def test_database_exists_returns_true_when_databae_exists + config = ActiveRecord::Base.configurations["arunit"] + assert SQLite3Adapter.database_exists?(config), + "expected #{config[:database]} to exist" + end + unless in_memory_db? def test_connect_with_url original_connection = ActiveRecord::Base.remove_connection @@ -51,15 +64,20 @@ module ActiveRecord end end + def test_database_exists_returns_true_for_an_in_memory_db + assert SQLite3Adapter.database_exists?(database: ":memory:"), + "Expected in memory database to exist" + end + def test_column_types owner = Owner.create!(name: "hello".encode("ascii-8bit")) owner.reload select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ", " - result = Owner.connection.exec_query <<-esql + result = Owner.connection.exec_query <<~SQL SELECT #{select} FROM #{Owner.table_name} WHERE #{Owner.primary_key} = #{owner.id} - esql + SQL assert_not(result.rows.first.include?("blob"), "should not store blobs") ensure @@ -160,13 +178,13 @@ module ActiveRecord end def test_quote_binary_column_escapes_it - DualEncoding.connection.execute(<<-eosql) + DualEncoding.connection.execute(<<~SQL) CREATE TABLE IF NOT EXISTS dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, name varchar(255), data binary ) - eosql + SQL str = (+"\x80").force_encoding("ASCII-8BIT") binary = DualEncoding.new name: "いただきます!", data: str binary.save! @@ -261,7 +279,7 @@ module ActiveRecord end def test_tables_logs_name - sql = <<-SQL + sql = <<~SQL SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do @@ -271,7 +289,7 @@ module ActiveRecord def test_table_exists_logs_name with_example_table do - sql = <<-SQL + sql = <<~SQL SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do @@ -536,10 +554,6 @@ module ActiveRecord end end - def test_deprecate_valid_alter_table_type - assert_deprecated { @conn.valid_alter_table_type?(:string) } - end - def test_db_is_not_readonly_when_readonly_option_is_false conn = Base.sqlite3_connection database: ":memory:", adapter: "sqlite3", @@ -573,8 +587,73 @@ module ActiveRecord end end - private + def test_errors_when_an_insert_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + end + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.execute("UPDATE ex SET data = '9989' WHERE data = '138853948594'") + end + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.execute("DELETE FROM ex where data = '138853948594'") + end + end + end + end + + def test_errors_when_a_replace_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection_handler.while_preventing_writes do + @conn.execute("REPLACE INTO ex (data) VALUES ('249823948')") + end + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @connection_handler.while_preventing_writes do + assert_equal 1, @conn.execute("SELECT data from ex WHERE data = '138853948594'").count + end + end + end + + def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @connection_handler.while_preventing_writes do + assert_equal 1, @conn.execute(" SELECT data from ex WHERE data = '138853948594'").count + end + end + end + + private def assert_logged(logs) subscriber = SQLSubscriber.new subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) @@ -585,7 +664,7 @@ module ActiveRecord end def with_example_table(definition = nil, table_name = "ex", &block) - definition ||= <<-SQL + definition ||= <<~SQL id integer PRIMARY KEY AUTOINCREMENT, number integer SQL diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb index d70486605f..cfc9853aba 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -8,17 +8,15 @@ module ActiveRecord class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase def test_sqlite_creates_directory Dir.mktmpdir do |dir| - begin - dir = Pathname.new(dir) - @conn = Base.sqlite3_connection database: dir.join("db/foo.sqlite3"), - adapter: "sqlite3", - timeout: 100 + dir = Pathname.new(dir) + @conn = Base.sqlite3_connection database: dir.join("db/foo.sqlite3"), + adapter: "sqlite3", + timeout: 100 - assert Dir.exist? dir.join("db") - assert File.exist? dir.join("db/foo.sqlite3") - ensure - @conn.disconnect! if @conn - end + assert Dir.exist? dir.join("db") + assert File.exist? dir.join("db/foo.sqlite3") + ensure + @conn.disconnect! if @conn end end end diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index fbdf2ada4b..d270175af4 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -27,7 +27,7 @@ class AggregationsTest < ActiveRecord::TestCase def test_immutable_value_objects customers(:david).balance = Money.new(100) - assert_raise(frozen_error_class) { customers(:david).balance.instance_eval { @amount = 20 } } + assert_raise(FrozenError) { customers(:david).balance.instance_eval { @amount = 20 } } end def test_inferred_mapping diff --git a/activerecord/test/cases/annotate_test.rb b/activerecord/test/cases/annotate_test.rb new file mode 100644 index 0000000000..4d71d28f83 --- /dev/null +++ b/activerecord/test/cases/annotate_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +class AnnotateTest < ActiveRecord::TestCase + fixtures :posts + + def test_annotate_wraps_content_in_an_inline_comment + quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts") + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + posts = Post.select(:id).annotate("foo") + assert posts.first + end + end + + def test_annotate_is_sanitized + quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts") + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + posts = Post.select(:id).annotate("*/foo/*") + assert posts.first + end + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + posts = Post.select(:id).annotate("**//foo//**") + assert posts.first + end + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/ /\* bar \*/}i) do + posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar") + assert posts.first + end + + assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \+ MAX_EXECUTION_TIME\(1\) \*/}i) do + posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)") + assert posts.first + end + end + + private + def regexp_escape_table_name(name) + Regexp.escape(Post.connection.quote_table_name(name)) + end +end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index f05dcac7dd..2d5a06a4ac 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -9,7 +9,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase @original_verbose = ActiveRecord::Migration.verbose ActiveRecord::Migration.verbose = false @connection = ActiveRecord::Base.connection - ActiveRecord::SchemaMigration.drop_table + @schema_migration = @connection.schema_migration + @schema_migration.drop_table end teardown do @@ -18,21 +19,21 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase @connection.drop_table :nep_schema_migrations rescue nil @connection.drop_table :has_timestamps rescue nil @connection.drop_table :multiple_indexes rescue nil - ActiveRecord::SchemaMigration.delete_all rescue nil + @schema_migration.delete_all rescue nil ActiveRecord::Migration.verbose = @original_verbose end def test_has_primary_key old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - assert_equal "version", ActiveRecord::SchemaMigration.primary_key + assert_equal "version", @schema_migration.primary_key - ActiveRecord::SchemaMigration.create_table - assert_difference "ActiveRecord::SchemaMigration.count", 1 do - ActiveRecord::SchemaMigration.create version: 12 + @schema_migration.create_table + assert_difference "@schema_migration.count", 1 do + @schema_migration.create version: 12 end ensure - ActiveRecord::SchemaMigration.drop_table + @schema_migration.drop_table ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type end @@ -51,11 +52,11 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase assert_equal 7, @connection.migration_context.current_version end - def test_schema_define_w_table_name_prefix - table_name = ActiveRecord::SchemaMigration.table_name + def test_schema_define_with_table_name_prefix old_table_name_prefix = ActiveRecord::Base.table_name_prefix ActiveRecord::Base.table_name_prefix = "nep_" - ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" + @schema_migration.reset_table_name + ActiveRecord::InternalMetadata.reset_table_name ActiveRecord::Schema.define(version: 7) do create_table :fruits do |t| t.column :color, :string @@ -67,7 +68,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase assert_equal 7, @connection.migration_context.current_version ensure ActiveRecord::Base.table_name_prefix = old_table_name_prefix - ActiveRecord::SchemaMigration.table_name = table_name + @schema_migration.reset_table_name + ActiveRecord::InternalMetadata.reset_table_name end def test_schema_raises_an_error_for_invalid_column_type @@ -88,10 +90,10 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end def test_normalize_version - assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") - assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") - assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") - assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") + assert_equal "118", @schema_migration.normalize_migration_number("0000118") + assert_equal "002", @schema_migration.normalize_migration_number("2") + assert_equal "017", @schema_migration.normalize_migration_number("0017") + assert_equal "20131219224947", @schema_migration.normalize_migration_number("20131219224947") end def test_schema_load_with_multiple_indexes_for_column_of_different_names @@ -116,8 +118,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) end def test_timestamps_without_null_set_null_to_false_on_change_table @@ -129,8 +131,23 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps, bulk: true do |t| + t.timestamps default: Time.now + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) + end end def test_timestamps_without_null_set_null_to_false_on_add_timestamps @@ -139,7 +156,58 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase add_timestamps :has_timestamps, default: Time.now end - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) + end + + if subsecond_precision_supported? + def test_timestamps_sets_precision_on_create_table + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end + + def test_timestamps_sets_precision_on_change_table + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps do |t| + t.timestamps default: Time.now + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_sets_precision_on_change_table_with_bulk + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps, bulk: true do |t| + t.timestamps default: Time.now + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end + end + + def test_timestamps_sets_precision_on_add_timestamps + ActiveRecord::Schema.define do + create_table :has_timestamps + add_timestamps :has_timestamps, default: Time.now + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end end end diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb index 671e273543..7ebb90c6fd 100644 --- a/activerecord/test/cases/arel/attributes/attribute_test.rb +++ b/activerecord/test/cases/arel/attributes/attribute_test.rb @@ -560,7 +560,7 @@ module Arel end end - describe "with a range" do + describe "#between" do it "can be constructed with a standard range" do attribute = Attribute.new nil, nil node = attribute.between(1..3) @@ -628,7 +628,6 @@ module Arel node.must_equal Nodes::NotIn.new(attribute, []) end - it "can be constructed with a range ending at Infinity" do attribute = Attribute.new nil, nil node = attribute.between(0..::Float::INFINITY) @@ -639,6 +638,30 @@ module Arel ) end + if Gem::Version.new("2.7.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly starting at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(eval("..0")) # eval for backwards compatibility + + node.must_equal Nodes::LessThanOrEqual.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + + if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser + + node.must_equal Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + it "can be constructed with a quoted range ending at Infinity" do attribute = Attribute.new nil, nil node = attribute.between(quoted_range(0, ::Float::INFINITY, false)) @@ -664,14 +687,6 @@ module Arel ) ]) end - - def quoted_range(begin_val, end_val, exclude) - OpenStruct.new( - begin: Nodes::Quoted.new(begin_val), - end: Nodes::Quoted.new(end_val), - exclude_end?: exclude, - ) - end end describe "#in" do @@ -753,21 +768,23 @@ module Arel end end - describe "with a range" do + describe "#not_between" do it "can be constructed with a standard range" do attribute = Attribute.new nil, nil node = attribute.not_between(1..3) - node.must_equal Nodes::Grouping.new(Nodes::Or.new( - Nodes::LessThan.new( - attribute, - Nodes::Casted.new(1, attribute) - ), - Nodes::GreaterThan.new( - attribute, - Nodes::Casted.new(3, attribute) + node.must_equal Nodes::Grouping.new( + Nodes::Or.new( + Nodes::LessThan.new( + attribute, + Nodes::Casted.new(1, attribute) + ), + Nodes::GreaterThan.new( + attribute, + Nodes::Casted.new(3, attribute) + ) ) - )) + ) end it "can be constructed with a range starting from -Infinity" do @@ -780,6 +797,16 @@ module Arel ) end + it "can be constructed with a quoted range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, false)) + + node.must_equal Nodes::GreaterThan.new( + attribute, + Nodes::Quoted.new(3) + ) + end + it "can be constructed with an exclusive range starting from -Infinity" do attribute = Attribute.new nil, nil node = attribute.not_between(-::Float::INFINITY...3) @@ -790,6 +817,16 @@ module Arel ) end + it "can be constructed with a quoted exclusive range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, true)) + + node.must_equal Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Quoted.new(3) + ) + end + it "can be constructed with an infinite range" do attribute = Attribute.new nil, nil node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY) @@ -797,6 +834,13 @@ module Arel node.must_equal Nodes::In.new(attribute, []) end + it "can be constructed with a quoted infinite range" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false)) + + node.must_equal Nodes::In.new(attribute, []) + end + it "can be constructed with a range ending at Infinity" do attribute = Attribute.new nil, nil node = attribute.not_between(0..::Float::INFINITY) @@ -807,20 +851,56 @@ module Arel ) end + if Gem::Version.new("2.7.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly starting at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(eval("..0")) # eval for backwards compatibility + + node.must_equal Nodes::GreaterThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + + if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser + + node.must_equal Nodes::LessThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + + it "can be constructed with a quoted range ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(0, ::Float::INFINITY, false)) + + node.must_equal Nodes::LessThan.new( + attribute, + Nodes::Quoted.new(0) + ) + end + it "can be constructed with an exclusive range" do attribute = Attribute.new nil, nil node = attribute.not_between(0...3) - node.must_equal Nodes::Grouping.new(Nodes::Or.new( - Nodes::LessThan.new( - attribute, - Nodes::Casted.new(0, attribute) - ), - Nodes::GreaterThanOrEqual.new( - attribute, - Nodes::Casted.new(3, attribute) + node.must_equal Nodes::Grouping.new( + Nodes::Or.new( + Nodes::LessThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ), + Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(3, attribute) + ) ) - )) + ) end end @@ -1010,6 +1090,15 @@ module Arel condition.to_sql.must_equal %("foo"."id" = (select 1)) end end + + private + def quoted_range(begin_val, end_val, exclude) + OpenStruct.new( + begin: Nodes::Quoted.new(begin_val), + end: Nodes::Quoted.new(end_val), + exclude_end?: exclude, + ) + end end end end diff --git a/activerecord/test/cases/arel/attributes_test.rb b/activerecord/test/cases/arel/attributes_test.rb index b00af4bd29..1712633ae9 100644 --- a/activerecord/test/cases/arel/attributes_test.rb +++ b/activerecord/test/cases/arel/attributes_test.rb @@ -23,46 +23,5 @@ module Arel assert_equal 2, array.uniq.size end end - - describe "for" do - it "deals with unknown column types" do - column = Struct.new(:type).new :crazy - Attributes.for(column).must_equal Attributes::Undefined - end - - it "returns the correct constant for strings" do - [:string, :text, :binary].each do |type| - column = Struct.new(:type).new type - Attributes.for(column).must_equal Attributes::String - end - end - - it "returns the correct constant for ints" do - column = Struct.new(:type).new :integer - Attributes.for(column).must_equal Attributes::Integer - end - - it "returns the correct constant for floats" do - column = Struct.new(:type).new :float - Attributes.for(column).must_equal Attributes::Float - end - - it "returns the correct constant for decimals" do - column = Struct.new(:type).new :decimal - Attributes.for(column).must_equal Attributes::Decimal - end - - it "returns the correct constant for boolean" do - column = Struct.new(:type).new :boolean - Attributes.for(column).must_equal Attributes::Boolean - end - - it "returns the correct constant for time" do - [:date, :datetime, :timestamp, :time].each do |type| - column = Struct.new(:type).new type - Attributes.for(column).must_equal Attributes::Time - end - end - end end end diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb index 2376ad8d37..79b85742ee 100644 --- a/activerecord/test/cases/arel/insert_manager_test.rb +++ b/activerecord/test/cases/arel/insert_manager_test.rb @@ -11,19 +11,18 @@ module Arel end describe "insert" do - it "can create a Values node" do + it "can create a ValuesList node" do manager = Arel::InsertManager.new - values = manager.create_values %w{ a b }, %w{ c d } + values = manager.create_values_list([%w{ a b }, %w{ c d }]) - assert_kind_of Arel::Nodes::Values, values - assert_equal %w{ a b }, values.left - assert_equal %w{ c d }, values.right + assert_kind_of Arel::Nodes::ValuesList, values + assert_equal [%w{ a b }, %w{ c d }], values.rows end it "allows sql literals" do manager = Arel::InsertManager.new manager.into Table.new(:users) - manager.values = manager.create_values [Arel.sql("*")], %w{ a } + manager.values = manager.create_values([Arel.sql("*")]) manager.to_sql.must_be_like %{ INSERT INTO \"users\" VALUES (*) } @@ -186,9 +185,9 @@ module Arel manager = Arel::InsertManager.new manager.into table - manager.values = Nodes::Values.new [1] + manager.values = Nodes::ValuesList.new([[1], [2]]) manager.to_sql.must_be_like %{ - INSERT INTO "users" VALUES (1) + INSERT INTO "users" VALUES (1), (2) } end @@ -210,11 +209,11 @@ module Arel manager = Arel::InsertManager.new manager.into table - manager.values = Nodes::Values.new [1, "aaron"] + manager.values = Nodes::ValuesList.new([[1, "aaron"], [2, "david"]]) manager.columns << table[:id] manager.columns << table[:name] manager.to_sql.must_be_like %{ - INSERT INTO "users" ("id", "name") VALUES (1, 'aaron') + INSERT INTO "users" ("id", "name") VALUES (1, 'aaron'), (2, 'david') } end end diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb index eff54abd91..d123ca9fd0 100644 --- a/activerecord/test/cases/arel/nodes/and_test.rb +++ b/activerecord/test/cases/arel/nodes/and_test.rb @@ -16,6 +16,15 @@ module Arel assert_equal 2, array.uniq.size end end + + describe "functions as node expression" do + it "allows aliasing" do + aliased = And.new(["foo", "bar"]).as("baz") + + assert_kind_of As, aliased + assert_kind_of SqlLiteral, aliased.right + end + end end end end diff --git a/activerecord/test/cases/arel/nodes/case_test.rb b/activerecord/test/cases/arel/nodes/case_test.rb index 89861488df..946c2b0453 100644 --- a/activerecord/test/cases/arel/nodes/case_test.rb +++ b/activerecord/test/cases/arel/nodes/case_test.rb @@ -80,6 +80,16 @@ module Arel assert_equal 2, array.uniq.size end end + + describe "#as" do + it "allows aliasing" do + node = Case.new "foo" + as = node.as("bar") + + assert_equal node, as.left + assert_kind_of Arel::Nodes::SqlLiteral, as.right + end + end end end end diff --git a/activerecord/test/cases/arel/nodes/comment_test.rb b/activerecord/test/cases/arel/nodes/comment_test.rb new file mode 100644 index 0000000000..bf5eaf4c5a --- /dev/null +++ b/activerecord/test/cases/arel/nodes/comment_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative "../helper" +require "yaml" + +module Arel + module Nodes + class CommentTest < Arel::Spec + describe "equality" do + it "is equal with equal contents" do + array = [Comment.new(["foo"]), Comment.new(["foo"])] + assert_equal 1, array.uniq.size + end + + it "is not equal with different contents" do + array = [Comment.new(["foo"]), Comment.new(["bar"])] + assert_equal 2, array.uniq.size + end + end + end + end +end diff --git a/activerecord/test/cases/arel/nodes/select_core_test.rb b/activerecord/test/cases/arel/nodes/select_core_test.rb index 0b698205ff..6860f2a395 100644 --- a/activerecord/test/cases/arel/nodes/select_core_test.rb +++ b/activerecord/test/cases/arel/nodes/select_core_test.rb @@ -37,6 +37,7 @@ module Arel core1.groups = %w[j k l] core1.windows = %w[m n o] core1.havings = %w[p q r] + core1.comment = Arel::Nodes::Comment.new(["comment"]) core2 = SelectCore.new core2.froms = %w[a b c] core2.projections = %w[d e f] @@ -44,6 +45,7 @@ module Arel core2.groups = %w[j k l] core2.windows = %w[m n o] core2.havings = %w[p q r] + core2.comment = Arel::Nodes::Comment.new(["comment"]) array = [core1, core2] assert_equal 1, array.uniq.size end @@ -56,6 +58,7 @@ module Arel core1.groups = %w[j k l] core1.windows = %w[m n o] core1.havings = %w[p q r] + core1.comment = Arel::Nodes::Comment.new(["comment"]) core2 = SelectCore.new core2.froms = %w[a b c] core2.projections = %w[d e f] @@ -63,6 +66,11 @@ module Arel core2.groups = %w[j k l] core2.windows = %w[m n o] core2.havings = %w[l o l] + core2.comment = Arel::Nodes::Comment.new(["comment"]) + array = [core1, core2] + assert_equal 2, array.uniq.size + core2.havings = %w[p q r] + core2.comment = Arel::Nodes::Comment.new(["other"]) array = [core1, core2] assert_equal 2, array.uniq.size end diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb index 5220950905..e6c49cd429 100644 --- a/activerecord/test/cases/arel/select_manager_test.rb +++ b/activerecord/test/cases/arel/select_manager_test.rb @@ -1221,5 +1221,28 @@ module Arel manager.distinct_on(false).must_equal manager end end + + describe "comment" do + it "chains" do + manager = Arel::SelectManager.new + manager.comment("selecting").must_equal manager + end + + it "appends a comment to the generated query" do + manager = Arel::SelectManager.new + table = Table.new :users + manager.from(table).project(table["id"]) + + manager.comment("selecting") + manager.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" /* selecting */ + } + + manager.comment("selecting", "with", "comment") + manager.to_sql.must_be_like %{ + SELECT "users"."id" FROM "users" /* selecting */ /* with */ /* comment */ + } + end + end end end diff --git a/activerecord/test/cases/arel/support/fake_record.rb b/activerecord/test/cases/arel/support/fake_record.rb index 559ff5d4e6..5ebeabd4a3 100644 --- a/activerecord/test/cases/arel/support/fake_record.rb +++ b/activerecord/test/cases/arel/support/fake_record.rb @@ -58,6 +58,14 @@ module FakeRecord "\"#{name}\"" end + def sanitize_as_sql_comment(comment) + comment + end + + def in_clause_length + 3 + end + def schema_cache self end diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb index f94ad521d7..106be2311d 100644 --- a/activerecord/test/cases/arel/visitors/depth_first_test.rb +++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb @@ -33,6 +33,7 @@ module Arel Arel::Nodes::Ordering, Arel::Nodes::StringJoin, Arel::Nodes::UnqualifiedColumn, + Arel::Nodes::ValuesList, Arel::Nodes::Limit, Arel::Nodes::Else, ].each do |klass| @@ -100,6 +101,12 @@ module Arel assert_equal [:a, :b, join], @collector.calls end + def test_comment + comment = Nodes::Comment.new ["foo"] + @visitor.accept comment + assert_equal ["foo", ["foo"], comment], @collector.calls + end + [ Arel::Nodes::Assignment, Arel::Nodes::Between, @@ -116,7 +123,6 @@ module Arel Arel::Nodes::NotIn, Arel::Nodes::Or, Arel::Nodes::TableAlias, - Arel::Nodes::Values, Arel::Nodes::As, Arel::Nodes::DeleteStatement, Arel::Nodes::JoinSource, diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb index 6b3c132f83..ade53c358e 100644 --- a/activerecord/test/cases/arel/visitors/dot_test.rb +++ b/activerecord/test/cases/arel/visitors/dot_test.rb @@ -37,6 +37,7 @@ module Arel Arel::Nodes::Offset, Arel::Nodes::Ordering, Arel::Nodes::UnqualifiedColumn, + Arel::Nodes::ValuesList, Arel::Nodes::Limit, ].each do |klass| define_method("test_#{klass.name.gsub('::', '_')}") do @@ -61,7 +62,6 @@ module Arel Arel::Nodes::NotIn, Arel::Nodes::Or, Arel::Nodes::TableAlias, - Arel::Nodes::Values, Arel::Nodes::As, Arel::Nodes::DeleteStatement, Arel::Nodes::JoinSource, diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb index 4bfa799a96..fd19574876 100644 --- a/activerecord/test/cases/arel/visitors/to_sql_test.rb +++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb @@ -23,9 +23,9 @@ module Arel sql.must_be_like "?" end - it "does not quote BindParams used as part of a Values" do + it "does not quote BindParams used as part of a ValuesList" do bp = Nodes::BindParam.new(1) - values = Nodes::Values.new([bp]) + values = Nodes::ValuesList.new([[bp]]) sql = compile values sql.must_be_like "VALUES (?)" end @@ -395,6 +395,11 @@ module Arel compile(node).must_be_like %{ "users"."id" IN (1, 2, 3) } + + node = @attr.in [1, 2, 3, 4, 5] + compile(node).must_be_like %{ + ("users"."id" IN (1, 2, 3) OR "users"."id" IN (4, 5)) + } end it "should return 1=0 when empty right which is always false" do @@ -545,6 +550,11 @@ module Arel compile(node).must_be_like %{ "users"."id" NOT IN (1, 2, 3) } + + node = @attr.not_in [1, 2, 3, 4, 5] + compile(node).must_be_like %{ + "users"."id" NOT IN (1, 2, 3) AND "users"."id" NOT IN (4, 5) + } end it "should return 1=1 when empty right which is always true" do diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 93dd427951..3525fa2ab8 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -32,16 +32,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase :posts, :tags, :taggings, :comments, :sponsors, :members def test_belongs_to - firm = Client.find(3).firm - assert_not_nil firm - assert_equal companies(:first_firm).name, firm.name + client = Client.find(3) + first_firm = companies(:first_firm) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal first_firm, client.firm + assert_equal first_firm.name, client.firm.name + end end def test_assigning_belongs_to_on_destroyed_object client = Client.create!(name: "Client") client.destroy! - assert_raise(frozen_error_class) { client.firm = nil } - assert_raise(frozen_error_class) { client.firm = Firm.new(name: "Firm") } + assert_raise(FrozenError) { client.firm = nil } + assert_raise(FrozenError) { client.firm = Firm.new(name: "Firm") } end def test_eager_loading_wont_mutate_owner_record @@ -60,7 +63,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase ActiveRecord::SQLCounter.clear_log Client.find(3).firm ensure - assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query" + sql_log = ActiveRecord::SQLCounter.log + assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}" end def test_belongs_to_with_primary_key @@ -444,8 +448,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_with_select - assert_equal 1, Company.find(2).firm_with_select.attributes.size - assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size + assert_equal 1, Post.find(2).author_with_select.attributes.size + assert_equal 1, Post.includes(:author_with_select).find(2).author_with_select.attributes.size + end + + def test_custom_attribute_with_select + assert_equal 2, Company.find(2).firm_with_select.attributes.size + assert_equal 2, Company.includes(:firm_with_select).find(2).firm_with_select.attributes.size end def test_belongs_to_without_counter_cache_option @@ -1290,17 +1299,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_with_out_of_range_value_assigning - model = Class.new(Comment) do + model = Class.new(Author) do def self.name; "Temp"; end - validates :post, presence: true + validates :author_address, presence: true end - comment = model.new - comment.post_id = 9223372036854775808 # out of range in the bigint + author = model.new + author.author_address_id = 9223372036854775808 # out of range in the bigint - assert_nil comment.post - assert_not_predicate comment, :valid? - assert_equal [{ error: :blank }], comment.errors.details[:post] + assert_nil author.author_address + assert_not_predicate author, :valid? + assert_equal [{ error: :blank }], author.errors.details[:author_address] end def test_polymorphic_with_custom_primary_key diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index a9e22c7643..cbe48a374f 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -18,7 +18,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels - authors = Author.all.merge!(includes: { posts: :comments }, order: "authors.id").to_a + authors = Author.includes(posts: :comments).order(:id).to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -26,7 +26,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_and_one_level - authors = Author.all.merge!(includes: [{ posts: :comments }, :categorizations], order: "authors.id").to_a + authors = Author.includes({ posts: :comments }, :categorizations).order(:id).to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size @@ -36,9 +36,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations - authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a - assert_equal 3, assert_no_queries { authors.size } - assert_equal 10, assert_no_queries { authors[0].comments.size } + authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).order(:id).to_a + assert_equal 3, assert_queries(0) { authors.size } + assert_equal 10, assert_queries(0) { authors[0].comments.size } end def test_eager_association_loading_grafts_stashed_associations_to_correct_parent @@ -103,14 +103,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase firms = Firm.all.merge!(includes: { account: { firm: :account } }, order: "companies.id").to_a assert_equal 2, firms.size assert_equal firms.first.account, firms.first.account.firm.account - assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account } - assert_equal companies(:first_firm).account.firm.account, assert_no_queries { firms.first.account.firm.account } + assert_equal companies(:first_firm).account, assert_queries(0) { firms.first.account.firm.account } + assert_equal companies(:first_firm).account.firm.account, assert_queries(0) { firms.first.account.firm.account } end def test_eager_association_loading_with_has_many_sti topics = Topic.all.merge!(includes: :replies, order: "topics.id").to_a first, second, = topics(:first).replies.size, topics(:second).replies.size - assert_no_queries do + assert_queries(0) do assert_equal first, topics[0].replies.size assert_equal second, topics[1].replies.size end @@ -121,7 +121,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert reply.save topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a - assert_no_queries do + assert_queries(0) do assert_equal 2, topics[0].replies.size assert_equal 0, topics[1].replies.size end @@ -131,13 +131,13 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase replies = Reply.all.merge!(includes: :topic, order: "topics.id").to_a assert_includes replies, topics(:second) assert_not_includes replies, topics(:first) - assert_equal topics(:first), assert_no_queries { replies.first.topic } + assert_equal topics(:first), assert_queries(0) { replies.first.topic } end def test_eager_association_loading_with_multiple_stis_and_order author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first assert_equal authors(:david), author - assert_no_queries do + assert_queries(0) do author.posts.first.special_comments author.posts.first.very_special_comment end @@ -146,7 +146,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_of_stis_with_multiple_references authors = Author.all.merge!(includes: { posts: { special_comments: { post: [ :special_comments, :very_special_comment ] } } }, order: "comments.body, very_special_comments_posts.body", where: "posts.id = 4").to_a assert_equal [authors(:david)], authors - assert_no_queries do + assert_queries(0) do authors.first.posts.first.special_comments.first.post.special_comments authors.first.posts.first.special_comments.first.post.very_special_comment end @@ -155,14 +155,14 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_where_first_level_returns_nil authors = Author.all.merge!(includes: { post_about_thinking: :comments }, order: "authors.id DESC").to_a assert_equal [authors(:bob), authors(:mary), authors(:david)], authors - assert_no_queries do + assert_queries(0) do authors[2].post_about_thinking.comments.first end end def test_preload_through_missing_records post = Post.where.not(author_id: Author.select(:id)).preload(author: { comments: :post }).first! - assert_no_queries { assert_nil post.author } + assert_queries(0) { assert_nil post.author } end def test_eager_association_loading_with_missing_first_record @@ -172,12 +172,12 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through source = Vertex.all.merge!(includes: { sinks: { sinks: { sinks: :sinks } } }, order: "vertices.id").first - assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first } + assert_equal vertices(:vertex_4), assert_queries(0) { source.sinks.first.sinks.first.sinks.first } end def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many sink = Vertex.all.merge!(includes: { sources: { sources: { sources: :sources } } }, order: "vertices.id DESC").first - assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first } + assert_equal vertices(:vertex_1), assert_queries(0) { sink.sources.first.sources.first.sources.first.sources.first } end def test_eager_association_loading_with_cascaded_interdependent_one_level_and_two_levels diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 5fca972aee..673d5f1dcf 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -21,7 +21,7 @@ module PolymorphicFullStiClassNamesSharedTest ActiveRecord::Base.store_full_sti_class = store_full_sti_class post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1) - @tagging = Tagging.create(taggable: post) + @tagging = post.create_tagging! end def teardown diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index 525ad3197a..9be21b23db 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -14,6 +14,7 @@ module Remembered included do after_create :remember + private def remember; self.class.remembered << self; end end @@ -110,10 +111,10 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase end teardown do - @davey_mcdave.destroy - @first_post.destroy @first_comment.destroy @first_categorization.destroy + @davey_mcdave.destroy + @first_post.destroy end def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index b37e59038e..cb46f9e053 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/post" require "models/tagging" require "models/tag" +require "models/rating" require "models/comment" require "models/author" require "models/essay" @@ -44,7 +45,7 @@ end class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, - :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations, + :companies, :accounts, :tags, :taggings, :ratings, :people, :readers, :categorizations, :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors @@ -89,6 +90,28 @@ class EagerAssociationTest < ActiveRecord::TestCase "expected to find only david's posts" end + def test_loading_polymorphic_association_with_mixed_table_conditions + rating = Rating.first + assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag + + rating = Rating.preload(:taggings_without_tag).first + assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag + + rating = Rating.eager_load(:taggings_without_tag).first + assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag + end + + def test_loading_association_with_string_joins + rating = Rating.first + assert_equal [taggings(:normal_comment_rating)], rating.taggings_with_no_tag + + rating = Rating.preload(:taggings_with_no_tag).first + assert_equal [taggings(:normal_comment_rating)], rating.taggings_with_no_tag + + rating = Rating.eager_load(:taggings_with_no_tag).first + assert_equal [taggings(:normal_comment_rating)], rating.taggings_with_no_tag + end + def test_loading_with_scope_including_joins member = Member.first assert_equal members(:groucho), member @@ -228,7 +251,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_load_associated_records_in_one_query_when_adapter_has_no_limit - assert_called(Comment.connection, :in_clause_length, returns: nil) do + assert_not_called(Comment.connection, :in_clause_length) do post = posts(:welcome) assert_queries(2) do Post.includes(:comments).where(id: post.id).to_a @@ -237,16 +260,16 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_load_associated_records_in_several_queries_when_many_ids_passed - assert_called(Comment.connection, :in_clause_length, returns: 1) do + assert_called(Comment.connection, :in_clause_length, times: 2, returns: 1) do post1, post2 = posts(:welcome), posts(:thinking) - assert_queries(3) do + assert_queries(2) do Post.includes(:comments).where(id: [post1.id, post2.id]).to_a end end end def test_load_associated_records_in_one_query_when_a_few_ids_passed - assert_called(Comment.connection, :in_clause_length, returns: 3) do + assert_not_called(Comment.connection, :in_clause_length) do post = posts(:welcome) assert_queries(2) do Post.includes(:comments).where(id: post.id).to_a @@ -500,7 +523,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id") assert_nothing_raised do - Comment.includes(:post).references(:posts).order(Arel.sql(quoted_posts_id)) + Comment.includes(:post).references(:posts).order(quoted_posts_id) end end @@ -766,7 +789,6 @@ class EagerAssociationTest < ActiveRecord::TestCase .where("comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'") .references(:comments) .scoping do - posts = authors(:david).posts.limit(2).to_a assert_equal 2, posts.size end @@ -775,7 +797,6 @@ class EagerAssociationTest < ActiveRecord::TestCase .where("authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')") .references(:authors, :comments) .scoping do - count = Post.limit(2).count assert_equal count, posts.size end @@ -947,14 +968,14 @@ class EagerAssociationTest < ActiveRecord::TestCase posts(:thinking, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: Arel.sql("UPPER(posts.title)"), limit: 2, offset: 1 + order: "UPPER(posts.title)", limit: 2, offset: 1 ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: Arel.sql("UPPER(posts.title) DESC"), limit: 2, offset: 1 + order: "UPPER(posts.title) DESC", limit: 2, offset: 1 ).to_a ) end @@ -964,14 +985,14 @@ class EagerAssociationTest < ActiveRecord::TestCase posts(:thinking, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: [Arel.sql("UPPER(posts.title)"), "posts.id"], limit: 2, offset: 1 + order: ["UPPER(posts.title)", "posts.id"], limit: 2, offset: 1 ).to_a ) assert_equal( posts(:sti_post_and_comments, :sti_comments), Post.all.merge!( includes: [:author, :comments], where: { "authors.name" => "David" }, - order: [Arel.sql("UPPER(posts.title) DESC"), "posts.id"], limit: 2, offset: 1 + order: ["UPPER(posts.title) DESC", "posts.id"], limit: 2, offset: 1 ).to_a ) end @@ -1222,7 +1243,7 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.all.merge!(select: "posts.*, authors.name as author_name", includes: :comments, joins: :author, order: "posts.id").to_a end assert_equal "David", posts[0].author_name - assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments } + assert_equal posts(:welcome).comments.sort_by(&:id), assert_no_queries { posts[0].comments.sort_by(&:id) } end def test_eager_loading_with_conditions_on_join_model_preloads @@ -1234,8 +1255,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_belongs_to_uses_exclusive_scope - people = Person.males.merge(includes: :primary_contact).to_a - assert_not_equal people.length, 0 + people = Person.males.includes(:primary_contact).to_a + assert_equal 2, people.length people.each do |person| assert_no_queries { assert_not_nil person.primary_contact } assert_equal Person.find(person.id).primary_contact, person.primary_contact @@ -1244,27 +1265,23 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preload_has_many_uses_exclusive_scope people = Person.males.includes(:agents).to_a + assert_equal 2, people.length people.each do |person| - assert_equal Person.find(person.id).agents, person.agents + assert_equal Person.find(person.id).agents.sort_by(&:id), person.agents.sort_by(&:id) end end def test_preload_has_many_using_primary_key - expected = Firm.first.clients_using_primary_key.to_a + expected = Firm.first.clients_using_primary_key.sort_by(&:id) firm = Firm.includes(:clients_using_primary_key).first assert_no_queries do - assert_equal expected, firm.clients_using_primary_key + assert_equal expected, firm.clients_using_primary_key.sort_by(&:id) end end def test_include_has_many_using_primary_key expected = Firm.find(1).clients_using_primary_key.sort_by(&:name) - # Oracle adapter truncates alias to 30 characters - if current_adapter?(:OracleAdapter) - firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0, 30] + ".name").find(1) - else - firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1) - end + firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1) assert_no_queries do assert_equal expected, firm.clients_using_primary_key end @@ -1393,11 +1410,24 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal expected, FirstPost.unscoped.find(2) end - test "preload ignores the scoping" do - assert_equal( - Comment.find(1).post, - Post.where("1 = 0").scoping { Comment.preload(:post).find(1).post } - ) + test "belongs_to association ignores the scoping" do + post = Comment.find(1).post + + Post.where("1=0").scoping do + assert_equal post, Comment.find(1).post + assert_equal post, Comment.preload(:post).find(1).post + assert_equal post, Comment.eager_load(:post).find(1).post + end + end + + test "has_many association ignores the scoping" do + comments = Post.find(1).comments.to_a + + Comment.where("1=0").scoping do + assert_equal comments, Post.find(1).comments + assert_equal comments, Post.preload(:comments).find(1).comments + assert_equal comments, Post.eager_load(:comments).find(1).comments + end end test "deep preload" do @@ -1496,6 +1526,24 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_match message, error.message end + test "preloading and eager loading of optional instance dependent associations is not supported" do + message = "association scope 'posts_mentioning_author' is" + error = assert_raises(ArgumentError) do + Author.includes(:posts_mentioning_author).to_a + end + assert_match message, error.message + + error = assert_raises(ArgumentError) do + Author.preload(:posts_mentioning_author).to_a + end + assert_match message, error.message + + error = assert_raises(ArgumentError) do + Author.eager_load(:posts_mentioning_author).to_a + end + assert_match message, error.message + end + test "preload with invalid argument" do exception = assert_raises(ArgumentError) do Author.preload(10).to_a diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index aef8f31112..604a52655c 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -70,8 +70,8 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase extend!(Developer) extend!(MyApplication::Business::Developer) - assert Object.const_get "DeveloperAssociationNameAssociationExtension" - assert MyApplication::Business.const_get "DeveloperAssociationNameAssociationExtension" + assert Developer.const_get "AssociationNameAssociationExtension" + assert MyApplication::Business::Developer.const_get "AssociationNameAssociationExtension" end def test_proxy_association_after_scoped @@ -87,8 +87,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase end private - def extend!(model) - ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } + ActiveRecord::Associations::Builder::HasMany.send(:define_extensions, model, :association_name) { } end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 515eb65d37..25cfa0a723 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -313,10 +313,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) - # Load schema information so we don't query below if running just this test. - Project.define_attribute_methods - - proj = assert_no_queries { devel.projects.build("name" => "Projekt") } + proj = assert_queries(0) { devel.projects.build("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj @@ -332,10 +329,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) - # Load schema information so we don't query below if running just this test. - Project.define_attribute_methods - - proj = assert_no_queries { devel.projects.new("name" => "Projekt") } + proj = assert_queries(0) { devel.projects.new("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj @@ -556,7 +550,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first - assert_no_queries do + assert_queries(0) do assert_predicate project.developers, :loaded? assert_includes project.developers, developer end @@ -751,7 +745,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations developer = developers(:david) developer.projects.reload - assert_no_queries do + assert_queries(0) do developer.project_ids developer.project_ids end @@ -879,7 +873,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects - assert_no_queries do + assert_queries(0) do assert_equal [], projects assert_equal [], projects.where(title: "omg") assert_equal [], projects.pluck(:title) @@ -1007,16 +1001,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_has_and_belongs_to_many_while_partial_writes_false - begin - original_partial_writes = ActiveRecord::Base.partial_writes - ActiveRecord::Base.partial_writes = false - developer = Developer.new(name: "Mehmet Emin İNAÇ") - developer.projects << Project.new(name: "Bounty") - - assert developer.save - ensure - ActiveRecord::Base.partial_writes = original_partial_writes - end + original_partial_writes = ActiveRecord::Base.partial_writes + ActiveRecord::Base.partial_writes = false + developer = Developer.new(name: "Mehmet Emin İNAÇ") + developer.projects << Project.new(name: "Bounty") + + assert developer.save + ensure + ActiveRecord::Base.partial_writes = original_partial_writes end def test_has_and_belongs_to_many_with_belongs_to diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index d13e1a86e9..6c54c2f1cd 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -27,6 +27,7 @@ require "models/categorization" require "models/minivan" require "models/speedometer" require "models/reference" +require "models/job" require "models/college" require "models/student" require "models/pirate" @@ -264,7 +265,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create(name: "honda") car.funky_bulbs.create! assert_equal 1, car.funky_bulbs.count - assert_nothing_raised { car.reload.funky_bulbs.delete_all } + assert_equal 1, car.reload.funky_bulbs.delete_all assert_equal 0, car.funky_bulbs.count, "bulbs should have been deleted using :delete_all strategy" end @@ -294,6 +295,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal(expected_sql, loaded_sql) end + def test_delete_all_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + posts.create!(title: "test", body: "body") + posts.delete_all + assert_nil posts.first + end + def test_building_the_associated_object_with_implicit_sti_base_class firm = DependentFirm.new company = firm.companies.build @@ -459,10 +468,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase company = companies(:first_firm) new_clients = [] - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - assert_no_queries do + assert_queries(0) do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") new_clients << company.clients_of_firm.build(name: "Another Client III") @@ -483,10 +489,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase company = companies(:first_firm) new_clients = [] - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - assert_no_queries do + assert_queries(0) do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") new_clients << company.clients_of_firm.build(name: "Another Client III") @@ -987,9 +990,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_predicate companies(:first_firm).clients_of_firm, :loaded? - companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) + result = companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) assert_equal 4, companies(:first_firm).clients_of_firm.size assert_equal 4, companies(:first_firm).clients_of_firm.reload.size + assert_equal companies(:first_firm).clients_of_firm, result end def test_transactions_when_adding_to_persisted @@ -1005,11 +1009,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_adding_to_new_record - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - firm = Firm.new - assert_no_queries do + assert_queries(0) do firm.clients_of_firm.concat(Client.new("name" => "Natural Company")) end end @@ -1024,10 +1025,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } + new_client = assert_queries(0) { company.clients_of_firm.new("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -1038,10 +1036,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } + new_client = assert_queries(0) { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -1099,10 +1094,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - new_clients = assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } + new_clients = assert_queries(0) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } assert_equal 2, new_clients.size end @@ -1117,10 +1109,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, first_topic.replies.length - # Load schema information so we don't query below if running just this test. - Reply.define_attribute_methods - - assert_no_queries do + assert_queries(0) do first_topic.replies.build(title: "Not saved", content: "Superstars") assert_equal 2, first_topic.replies.size end @@ -1131,10 +1120,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_via_block company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } + new_client = assert_queries(0) { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -1145,10 +1131,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many_via_block company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - new_clients = assert_no_queries do + new_clients = assert_queries(0) do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end @@ -1215,7 +1198,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_has_many_without_counter_cache_option # Ship has a conventionally named `treasures_count` column, but the counter_cache # option is not given on the association. - ship = Ship.create(name: "Countless", treasures_count: 10) + ship = Ship.create!(name: "Countless", treasures_count: 10) assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter? @@ -1405,7 +1388,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, clients.count assert_difference "Client.count", -(clients.count) do - companies(:first_firm).dependent_clients_of_firm.delete_all + assert_equal clients.count, companies(:first_firm).dependent_clients_of_firm.delete_all end end @@ -1437,11 +1420,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transaction_when_deleting_new_record - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - firm = Firm.new - assert_no_queries do + assert_queries(0) do client = Client.new("name" => "New Client") firm.clients_of_firm << client firm.clients_of_firm.destroy(client) @@ -1502,10 +1482,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all_with_option_delete_all firm = companies(:first_firm) client_id = firm.dependent_clients_of_firm.first.id - firm.dependent_clients_of_firm.delete_all(:delete_all) + count = firm.dependent_clients_of_firm.count + assert_equal count, firm.dependent_clients_of_firm.delete_all(:delete_all) assert_nil Client.find_by_id(client_id) end + def test_delete_all_with_option_nullify + firm = companies(:first_firm) + client_id = firm.dependent_clients_of_firm.first.id + count = firm.dependent_clients_of_firm.count + assert_equal firm, Client.find(client_id).firm + assert_equal count, firm.dependent_clients_of_firm.delete_all(:nullify) + assert_nil Client.find(client_id).firm + end + def test_delete_all_accepts_limited_parameters firm = companies(:first_firm) assert_raise(ArgumentError) do @@ -1710,6 +1700,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh" end + def test_destroy_all_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + posts.create!(title: "test", body: "body") + posts.destroy_all + assert_nil posts.first + end + + def test_destroy_all_on_desynced_counter_cache_association + category = categories(:general) + assert_operator category.categorizations.count, :>, 0 + + category.categorizations.destroy_all + assert_equal 0, category.categorizations.count + end + + def test_destroy_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + post = posts.create!(title: "test", body: "body") + posts.destroy(post) + assert_nil posts.first + end + + def test_delete_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + post = posts.create!(title: "test", body: "body") + posts.delete(post) + assert_nil posts.first + end + def test_dependence firm = companies(:first_firm) assert_equal 3, firm.clients.size @@ -1773,6 +1795,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal num_accounts, Account.count end + def test_depends_and_nullify_on_polymorphic_assoc + author = PersonWithPolymorphicDependentNullifyComments.create!(first_name: "Laertis") + comment = posts(:welcome).comments.first + comment.author = author + comment.save! + + assert_equal comment.author_id, author.id + assert_equal comment.author_type, author.class.name + + author.destroy + comment.reload + + assert_nil comment.author_id + assert_nil comment.author_type + end + def test_restrict_with_exception firm = RestrictedWithExceptionFirm.create!(name: "restrict") firm.companies.create(name: "child") @@ -1898,11 +1936,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_replacing_on_new_record - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - firm = Firm.new - assert_no_queries do + assert_queries(0) do firm.clients_of_firm = [Client.new("name" => "New Client")] end end @@ -1929,7 +1964,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_counter_cache_on_unloaded_association car = Car.create(name: "My AppliCar") - assert_equal car.engines.size, 0 + assert_equal 0, car.engines.size + end + + def test_ids_reader_cache_not_used_for_size_when_association_is_dirty + firm = Firm.create!(name: "Startup") + assert_equal 0, firm.client_ids.size + firm.clients.build + assert_equal 1, firm.clients.size + end + + def test_ids_reader_cache_should_be_cleared_when_collection_is_deleted + firm = companies(:first_firm) + assert_equal [2, 3, 11], firm.client_ids + client = firm.clients.first + firm.clients.delete(client) + assert_equal [3, 11], firm.client_ids end def test_get_ids_ignores_include_option @@ -1941,11 +1991,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids_for_association_on_new_record_does_not_try_to_find_records - # Load schema information so we don't query below if running just this test. - companies(:first_client).contract_ids - company = Company.new - assert_no_queries do + assert_queries(0) do company.contract_ids end @@ -1990,10 +2037,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_associations_order_should_be_priority_over_throughs_order - david = authors(:david) + original = authors(:david) expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1] - assert_equal expected, david.comments_desc.map(&:id) - assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id) + assert_equal expected, original.comments_desc.map(&:id) + preloaded = Author.includes(:comments_desc).find(original.id) + assert_equal expected, preloaded.comments_desc.map(&:id) + assert_equal original.posts_sorted_by_id.first.comments.map(&:id), preloaded.posts_sorted_by_id.first.comments.map(&:id) end def test_dynamic_find_should_respect_association_order_for_through @@ -2002,8 +2051,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_respects_hash_conditions - assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions - assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions + assert_equal authors(:david).hello_posts.sort_by(&:id), authors(:david).hello_posts_with_hash_conditions.sort_by(&:id) + assert_equal authors(:david).hello_post_comments.sort_by(&:id), authors(:david).hello_post_comments_with_hash_conditions.sort_by(&:id) end def test_include_uses_array_include_after_loaded @@ -2493,22 +2542,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "association with extend option" do post = posts(:welcome) - assert_equal "lifo", post.comments_with_extend.author - assert_equal "hello", post.comments_with_extend.greeting + assert_equal "lifo", post.comments_with_extend.author + assert_equal "hello :)", post.comments_with_extend.greeting end test "association with extend option with multiple extensions" do post = posts(:welcome) - assert_equal "lifo", post.comments_with_extend_2.author - assert_equal "hullo", post.comments_with_extend_2.greeting + assert_equal "lifo", post.comments_with_extend_2.author + assert_equal "hullo :)", post.comments_with_extend_2.greeting end test "extend option affects per association" do post = posts(:welcome) - assert_equal "lifo", post.comments_with_extend.author - assert_equal "lifo", post.comments_with_extend_2.author - assert_equal "hello", post.comments_with_extend.greeting - assert_equal "hullo", post.comments_with_extend_2.greeting + assert_equal "lifo", post.comments_with_extend.author + assert_equal "lifo", post.comments_with_extend_2.author + assert_equal "hello :)", post.comments_with_extend.greeting + assert_equal "hullo :)", post.comments_with_extend_2.greeting end test "delete record with complex joins" do @@ -2626,18 +2675,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase bulb = Bulb.create! tyre = Tyre.create! - car = Car.create! do |c| + car = Car.create!(name: "honda") do |c| c.bulbs << bulb c.tyres << tyre end + assert_equal [nil, "honda"], car.saved_change_to_name + assert_equal 1, car.bulbs.count assert_equal 1, car.tyres.count end test "associations replace in memory when records have the same id" do bulb = Bulb.create! - car = Car.create!(bulbs: [bulb]) + car = Car.create!(name: "honda", bulbs: [bulb]) + + assert_equal [nil, "honda"], car.saved_change_to_name new_bulb = Bulb.find(bulb.id) new_bulb.name = "foo" @@ -2648,7 +2701,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "in memory replacement executes no queries" do bulb = Bulb.create! - car = Car.create!(bulbs: [bulb]) + car = Car.create!(name: "honda", bulbs: [bulb]) + + assert_equal [nil, "honda"], car.saved_change_to_name new_bulb = Bulb.find(bulb.id) @@ -2680,7 +2735,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "in memory replacements sets inverse instance" do bulb = Bulb.create! - car = Car.create!(bulbs: [bulb]) + car = Car.create!(name: "honda", bulbs: [bulb]) + + assert_equal [nil, "honda"], car.saved_change_to_name new_bulb = Bulb.find(bulb.id) car.bulbs = [new_bulb] @@ -2700,7 +2757,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "in memory replacement maintains order" do first_bulb = Bulb.create! second_bulb = Bulb.create! - car = Car.create!(bulbs: [first_bulb, second_bulb]) + car = Car.create!(name: "honda", bulbs: [first_bulb, second_bulb]) + + assert_equal [nil, "honda"], car.saved_change_to_name same_bulb = Bulb.find(first_bulb.id) car.bulbs = [second_bulb, same_bulb] @@ -2868,8 +2927,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - private + def test_has_many_with_out_of_range_value + reference = Reference.create!(id: 2147483648) # out of range in the integer + assert_equal [], reference.ideal_jobs + end + def test_has_many_preloading_with_duplicate_records + posts = Post.joins(:comments).preload(:comments).to_a + assert_equal [1, 2], posts.first.comments.map(&:id) + end + + private def force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.load_target end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 7b405c74c4..6faa9664f7 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -33,6 +33,9 @@ require "models/organization" require "models/user" require "models/family" require "models/family_tree" +require "models/section" +require "models/seminar" +require "models/session" class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, @@ -46,11 +49,32 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create person_id: 0, post_id: 0 end + def test_has_many_through_create_record + assert books(:awdr).subscribers.create!(nick: "bob") + end + def test_marshal_dump preloaded = Post.includes(:first_blue_tags).first assert_equal preloaded, Marshal.load(Marshal.dump(preloaded)) end + def test_through_association_with_joins + assert_equal [comments(:eager_other_comment1)], authors(:mary).comments.merge(Post.joins(:comments)) + end + + def test_through_association_with_left_joins + assert_equal [comments(:eager_other_comment1)], authors(:mary).comments.merge(Post.left_joins(:comments)) + end + + def test_preload_with_nested_association + posts = Post.preload(:author, :author_favorites_with_scope).to_a + + assert_no_queries do + posts.each(&:author) + posts.each(&:author_favorites_with_scope) + end + end + def test_preload_sti_rhs_class developers = Developer.includes(:firms).all.to_a assert_no_queries do @@ -200,7 +224,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_no_difference "Job.count" do assert_difference "Reference.count", -1 do - person.reload.jobs_with_dependent_destroy.delete_all + assert_equal 1, person.reload.jobs_with_dependent_destroy.delete_all end end end @@ -211,7 +235,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_no_difference "Job.count" do assert_no_difference "Reference.count" do - person.reload.jobs_with_dependent_nullify.delete_all + assert_equal 1, person.reload.jobs_with_dependent_nullify.delete_all end end end @@ -222,17 +246,26 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_no_difference "Job.count" do assert_difference "Reference.count", -1 do - person.reload.jobs_with_dependent_delete_all.delete_all + assert_equal 1, person.reload.jobs_with_dependent_delete_all.delete_all end end end + def test_delete_all_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + people.create!(first_name: "Jeb") + people.delete_all + assert_nil people.first + end + def test_concat person = people(:david) post = posts(:thinking) - post.people.concat [person] + result = post.people.concat [person] assert_equal 1, post.people.size assert_equal 1, post.people.reload.size + assert_equal post.people, result end def test_associate_existing_record_twice_should_add_to_target_twice @@ -274,10 +307,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_queries(1) { posts(:thinking) } new_person = nil # so block binding catches it - # Load schema information so we don't query below if running just this test. - Person.define_attribute_methods - - assert_no_queries do + assert_queries(0) do new_person = Person.new first_name: "bob" end @@ -297,10 +327,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_associate_new_by_building assert_queries(1) { posts(:thinking) } - # Load schema information so we don't query below if running just this test. - Person.define_attribute_methods - - assert_no_queries do + assert_queries(0) do posts(:thinking).people.build(first_name: "Bob") posts(:thinking).people.new(first_name: "Ted") end @@ -366,7 +393,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_delete_association - assert_queries(2) { posts(:welcome);people(:michael); } + assert_queries(2) { posts(:welcome); people(:michael); } assert_queries(1) do posts(:welcome).people.delete(people(:michael)) @@ -401,6 +428,30 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_empty posts(:welcome).people.reload end + def test_destroy_all_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + people.create!(first_name: "Jeb") + people.destroy_all + assert_nil people.first + end + + def test_destroy_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + person = people.create!(first_name: "Jeb") + people.destroy(person) + assert_nil people.first + end + + def test_delete_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + person = people.create!(first_name: "Jeb") + people.delete(person) + assert_nil people.first + end + def test_should_raise_exception_for_destroying_mismatching_records assert_no_difference ["Person.count", "Reader.count"] do assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:welcome).people.destroy(posts(:thinking)) } @@ -569,7 +620,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_replace_association - assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload } + assert_queries(4) { posts(:welcome); people(:david); people(:michael); posts(:welcome).people.reload } # 1 query to delete the existing reader (michael) # 1 query to associate the new reader (david) @@ -586,6 +637,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_not_includes posts(:welcome).reload.people.reload, people(:michael) end + def test_replace_association_with_duplicates + post = posts(:thinking) + person = people(:david) + + assert_difference "post.people.count", 2 do + post.people = [person] + post.people = [person, person] + end + end + def test_replace_order_is_preserved posts(:welcome).people.clear posts(:welcome).people = [people(:david), people(:michael)] @@ -690,15 +751,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase firm = companies(:first_firm) lifo = Developer.new(name: "lifo") - assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo } + assert_raises(ActiveRecord::RecordInvalid) do + assert_deprecated { firm.developers << lifo } + end lifo = Developer.create!(name: "lifo") - assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo } + assert_raises(ActiveRecord::RecordInvalid) do + assert_deprecated { firm.developers << lifo } + end end end def test_clear_associations - assert_queries(2) { posts(:welcome);posts(:welcome).people.reload } + assert_queries(2) { posts(:welcome); posts(:welcome).people.reload } assert_queries(1) do posts(:welcome).people.clear @@ -1104,7 +1169,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_create_should_not_raise_exception_when_join_record_has_errors repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" } - Category.create(name: "Fishing", authors: [Author.first]) + assert_deprecated { Category.create(name: "Fishing", authors: [Author.first]) } end end @@ -1117,7 +1182,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" } assert_raises(ActiveRecord::RecordInvalid) do - Category.create!(name: "Fishing", authors: [Author.first]) + assert_deprecated { Category.create!(name: "Fishing", authors: [Author.first]) } end end end @@ -1127,7 +1192,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" } c = Category.new(name: "Fishing", authors: [Author.first]) assert_raises(ActiveRecord::RecordInvalid) do - c.save! + assert_deprecated { c.save! } end end end @@ -1136,7 +1201,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << "Invalid Categorization" } c = Category.new(name: "Fishing", authors: [Author.first]) - assert_not c.save + assert_deprecated { assert_not c.save } end end @@ -1418,6 +1483,37 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [subscription2], post.subscriptions.to_a end + def test_child_is_visible_to_join_model_in_add_association_callbacks + [:before_add, :after_add].each do |callback_name| + sentient_treasure = Class.new(Treasure) do + def self.name; "SentientTreasure"; end + + has_many :pet_treasures, foreign_key: :treasure_id, callback_name => :check_pet! + has_many :pets, through: :pet_treasures + + def check_pet!(added) + raise "No pet!" if added.pet.nil? + end + end + + treasure = sentient_treasure.new + assert_nothing_raised { treasure.pets << pets(:mochi) } + end + end + + def test_circular_autosave_association_correctly_saves_multiple_records + cs180 = Seminar.new(name: "CS180") + fall = Session.new(name: "Fall") + sections = [ + cs180.sections.build(short_name: "A"), + cs180.sections.build(short_name: "B"), + ] + fall.sections << sections + fall.save! + fall.reload + assert_equal sections, fall.sections.sort_by(&:id) + end + private def make_model(name) Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index adfb3ce072..3ef25c7027 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -12,26 +12,36 @@ require "models/bulb" require "models/author" require "models/image" require "models/post" +require "models/drink_designer" +require "models/chef" +require "models/department" +require "models/club" +require "models/membership" class HasOneAssociationsTest < ActiveRecord::TestCase self.use_transactional_tests = false unless supports_savepoints? - fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates, :authors, :author_addresses + fixtures :accounts, :companies, :developers, :projects, :developers_projects, + :ships, :pirates, :authors, :author_addresses, :memberships, :clubs def setup Account.destroyed_account_ids.clear end def test_has_one - assert_equal companies(:first_firm).account, Account.find(1) - assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit + firm = companies(:first_firm) + first_account = Account.find(1) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal first_account, firm.account + assert_equal first_account.credit_limit, firm.account.credit_limit + end end def test_has_one_does_not_use_order_by ActiveRecord::SQLCounter.clear_log companies(:first_firm).account ensure - log_all = ActiveRecord::SQLCounter.log_all - assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}" + sql_log = ActiveRecord::SQLCounter.log + assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}" end def test_has_one_cache_nils @@ -110,6 +120,21 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nil Account.find(old_account_id).firm_id end + def test_nullify_on_polymorphic_association + department = Department.create! + designer = DrinkDesignerWithPolymorphicDependentNullifyChef.create! + chef = department.chefs.create!(employable: designer) + + assert_equal chef.employable_id, designer.id + assert_equal chef.employable_type, designer.class.name + + designer.destroy! + chef.reload + + assert_nil chef.employable_id + assert_nil chef.employable_type + end + def test_nullification_on_destroyed_association developer = Developer.create!(name: "Someone") ship = Ship.create!(name: "Planet Caravan", developer: developer) @@ -231,11 +256,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_build_association_dont_create_transaction - # Load schema information so we don't query below if running just this test. - Account.define_attribute_methods - firm = Firm.new - assert_no_queries do + assert_queries(0) do firm.build_account end end @@ -684,6 +706,40 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_has_one_with_touch_option_on_create + assert_queries(3) { + Club.create(name: "1000 Oaks", membership_attributes: { favourite: true }) + } + end + + def test_has_one_with_touch_option_on_update + new_club = Club.create(name: "1000 Oaks") + new_club.create_membership + + assert_queries(2) { new_club.update(name: "Effingut") } + end + + def test_has_one_with_touch_option_on_touch + new_club = Club.create(name: "1000 Oaks") + new_club.create_membership + + assert_queries(1) { new_club.touch } + end + + def test_has_one_with_touch_option_on_destroy + new_club = Club.create(name: "1000 Oaks") + new_club.create_membership + + assert_queries(2) { new_club.destroy } + end + + def test_has_one_with_touch_option_on_empty_update + new_club = Club.create(name: "1000 Oaks") + new_club.create_membership + + assert_no_queries { new_club.save } + end + class SpecialBook < ActiveRecord::Base self.table_name = "books" belongs_to :author, class_name: "SpecialAuthor" diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 0309663943..69b4872519 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -35,6 +35,13 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:boring_club), @member.club end + def test_has_one_through_executes_limited_query + boring_club = clubs(:boring_club) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal boring_club, @member.general_club + end + end + def test_creating_association_creates_through_record new_member = Member.create(name: "Chris") new_member.club = Club.create(name: "LRUG") diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index c33dcdee61..e0dac01f4a 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -29,7 +29,10 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql - assert_match(/agents_people_4/i, sql) + assert_match(/agents_people_2/i, sql) + assert_match(/INNER JOIN/i, sql) + assert_no_match(/agents_people_4/i, sql) + assert_no_match(/LEFT OUTER JOIN/i, sql) end def test_construct_finder_sql_does_not_table_name_collide_with_string_joins diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index eb4dc73423..669e176dcb 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -8,6 +8,7 @@ require "models/zine" require "models/club" require "models/sponsor" require "models/rating" +require "models/post" require "models/comment" require "models/car" require "models/bulb" @@ -20,8 +21,6 @@ require "models/company" require "models/project" require "models/author" require "models/post" -require "models/department" -require "models/hotel" class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -64,6 +63,14 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection" end + def test_has_many_and_belongs_to_should_find_inverse_automatically_for_extension_block + comment_reflection = Comment.reflect_on_association(:post) + post_reflection = Post.reflect_on_association(:comments) + + assert_predicate post_reflection, :has_inverse? + assert_equal comment_reflection, post_reflection.inverse_of + end + def test_has_many_and_belongs_to_should_find_inverse_automatically_for_sti author_reflection = Author.reflect_on_association(:posts) author_child_reflection = Author.reflect_on_association(:special_posts) @@ -726,16 +733,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end - - def test_favors_has_one_associations_for_inverse_of - inverse_name = Post.reflect_on_association(:author).inverse_of.name - assert_equal :post, inverse_name - end - - def test_finds_inverse_of_for_plural_associations - inverse_name = Department.reflect_on_association(:hotel).inverse_of.name - assert_equal :departments, inverse_name - end end # NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb index 0e54e8c1b0..d44c6407f5 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -32,6 +32,10 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase assert_equal 17, Post.left_outer_joins(:comments).count end + def test_merging_left_joins_should_be_left_joins + assert_equal 5, Author.left_joins(:posts).merge(Post.no_comments).count + end + def test_left_joins_aliases_left_outer_joins assert_equal Post.left_outer_joins(:comments).to_sql, Post.left_joins(:comments).to_sql end @@ -46,6 +50,12 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase assert queries.any? { |sql| /LEFT OUTER JOIN/i.match?(sql) } end + def test_left_outer_joins_is_deduped_when_same_association_is_joined + queries = capture_sql { Author.joins(:posts).left_outer_joins(:posts).to_a } + assert queries.any? { |sql| /INNER JOIN/i.match?(sql) } + assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) } + end + def test_construct_finder_sql_ignores_empty_left_outer_joins_hash queries = capture_sql { Author.left_outer_joins({}).to_a } assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) } @@ -60,6 +70,10 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase assert_raise(ArgumentError) { Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a } end + def test_left_outer_joins_with_string_join + assert_equal 16, Author.left_outer_joins(:posts).joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id").count + end + def test_join_conditions_added_to_join_clause queries = capture_sql { Author.left_outer_joins(:essays).to_a } assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i.match?(sql) } diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 5821744530..8d74ae3961 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -548,6 +548,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end end + def test_through_association_preload_doesnt_reset_source_association_if_already_preloaded + blue = tags(:blue) + authors = Author.preload(posts: :first_blue_tags_2, misc_post_first_blue_tags_2: {}).to_a.sort_by(&:id) + + assert_no_queries do + assert_equal [blue], authors[2].posts.first.first_blue_tags_2 + end + end + def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins # Pointless condition to force single-query loading assert_includes_and_joins_equal( @@ -610,8 +619,13 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take end - private + def test_has_many_through_reset_source_reflection_after_loading_is_complete + preloaded = Category.preload(:ordered_post_comments).find(1, 2).last + original = Category.find(2) + assert_equal original.ordered_post_comments.ids, preloaded.ordered_post_comments.ids + end + private def assert_includes_and_joins_equal(query, expected, association) actual = assert_queries(1) { query.joins(association).to_a.uniq } assert_equal expected, actual diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index 65a3bb5efe..db7f945a36 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -25,20 +25,18 @@ class RequiredAssociationsTest < ActiveRecord::TestCase end test "belongs_to associations can be optional by default" do - begin - original_value = ActiveRecord::Base.belongs_to_required_by_default - ActiveRecord::Base.belongs_to_required_by_default = false + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = false - model = subclass_of(Child) do - belongs_to :parent, inverse_of: false, - class_name: "RequiredAssociationsTest::Parent" - end - - assert model.new.save - assert model.new(parent: Parent.new).save - ensure - ActiveRecord::Base.belongs_to_required_by_default = original_value + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" end + + assert model.new.save + assert model.new(parent: Parent.new).save + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value end test "required belongs_to associations have presence validated" do @@ -56,24 +54,22 @@ class RequiredAssociationsTest < ActiveRecord::TestCase end test "belongs_to associations can be required by default" do - begin - original_value = ActiveRecord::Base.belongs_to_required_by_default - ActiveRecord::Base.belongs_to_required_by_default = true + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true - model = subclass_of(Child) do - belongs_to :parent, inverse_of: false, - class_name: "RequiredAssociationsTest::Parent" - end + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end - record = model.new - assert_not record.save - assert_equal ["Parent must exist"], record.errors.full_messages + record = model.new + assert_not record.save + assert_equal ["Parent must exist"], record.errors.full_messages - record.parent = Parent.new - assert record.save - ensure - ActiveRecord::Base.belongs_to_required_by_default = original_value - end + record.parent = Parent.new + assert record.save + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value end test "has_one associations are not required by default" do @@ -121,7 +117,6 @@ class RequiredAssociationsTest < ActiveRecord::TestCase end private - def subclass_of(klass, &block) subclass = Class.new(klass, &block) def subclass.name diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 081da95df7..84130ec208 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -21,6 +21,11 @@ require "models/molecule" require "models/electron" require "models/man" require "models/interest" +require "models/pirate" +require "models/parrot" +require "models/bird" +require "models/treasure" +require "models/price_estimate" class AssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :developers_projects, @@ -368,3 +373,97 @@ class GeneratedMethodsTest < ActiveRecord::TestCase assert_equal :none, MyArticle.new.comments end end + +class WithAnnotationsTest < ActiveRecord::TestCase + fixtures :pirates, :parrots + + def test_belongs_to_with_annotation_includes_a_query_comment + pirate = SpacePirate.where.not(parrot_id: nil).first + assert pirate, "should have a Pirate record" + + log = capture_sql do + pirate.parrot + end + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty? + + assert_sql(%r{/\* that tells jokes \*/}) do + pirate.parrot_with_annotation + end + end + + def test_has_and_belongs_to_many_with_annotation_includes_a_query_comment + pirate = SpacePirate.first + assert pirate, "should have a Pirate record" + + log = capture_sql do + pirate.parrots.first + end + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty? + + assert_sql(%r{/\* that are very colorful \*/}) do + pirate.parrots_with_annotation.first + end + end + + def test_has_one_with_annotation_includes_a_query_comment + pirate = SpacePirate.first + assert pirate, "should have a Pirate record" + + log = capture_sql do + pirate.ship + end + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty? + + assert_sql(%r{/\* that is a rocket \*/}) do + pirate.ship_with_annotation + end + end + + def test_has_many_with_annotation_includes_a_query_comment + pirate = SpacePirate.first + assert pirate, "should have a Pirate record" + + log = capture_sql do + pirate.birds.first + end + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty? + + assert_sql(%r{/\* that are also parrots \*/}) do + pirate.birds_with_annotation.first + end + end + + def test_has_many_through_with_annotation_includes_a_query_comment + pirate = SpacePirate.first + assert pirate, "should have a Pirate record" + + log = capture_sql do + pirate.treasure_estimates.first + end + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty? + + assert_sql(%r{/\* yarrr \*/}) do + pirate.treasure_estimates_with_annotation.first + end + end + + def test_has_many_through_with_annotation_includes_a_query_comment_when_eager_loading + pirate = SpacePirate.first + assert pirate, "should have a Pirate record" + + log = capture_sql do + pirate.treasure_estimates.first + end + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty? + + assert_sql(%r{/\* yarrr \*/}) do + SpacePirate.includes(:treasure_estimates_with_annotation, :treasures).first + end + end +end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 0dbdd56ae6..71b5407dcc 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -323,6 +323,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_raises(ActiveModel::UnknownAttributeError) { topic.update(no_column_exists: "Hello!") } end + test "write_attribute allows writing to aliased attributes" do + topic = Topic.first + assert_nothing_raised { topic.update_columns(heading: "Hello!") } + assert_nothing_raised { topic.update(heading: "Hello!") } + end + test "read_attribute" do topic = Topic.new topic.title = "Don't change the topic" @@ -427,6 +433,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase end assert_equal true, Topic.new(author_name: "Name").author_name? + + ActiveModel::Type::Boolean::FALSE_VALUES.each do |value| + assert_predicate Topic.new(author_name: value), :author_name? + end end test "number attribute predicate" do @@ -448,8 +458,71 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + test "user-defined text attribute predicate" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = Topic.table_name + + attribute :user_defined_text, :text + end + + topic = klass.new(user_defined_text: "text") + assert_predicate topic, :user_defined_text? + + ActiveModel::Type::Boolean::FALSE_VALUES.each do |value| + topic = klass.new(user_defined_text: value) + assert_predicate topic, :user_defined_text? + end + end + + test "user-defined date attribute predicate" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = Topic.table_name + + attribute :user_defined_date, :date + end + + topic = klass.new(user_defined_date: Date.current) + assert_predicate topic, :user_defined_date? + end + + test "user-defined datetime attribute predicate" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = Topic.table_name + + attribute :user_defined_datetime, :datetime + end + + topic = klass.new(user_defined_datetime: Time.current) + assert_predicate topic, :user_defined_datetime? + end + + test "user-defined time attribute predicate" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = Topic.table_name + + attribute :user_defined_time, :time + end + + topic = klass.new(user_defined_time: Time.current) + assert_predicate topic, :user_defined_time? + end + + test "user-defined json attribute predicate" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = Topic.table_name + + attribute :user_defined_json, :json + end + + topic = klass.new(user_defined_json: { key: "value" }) + assert_predicate topic, :user_defined_json? + + topic = klass.new(user_defined_json: {}) + assert_not_predicate topic, :user_defined_json? + end + test "custom field attribute predicate" do - object = Company.find_by_sql(<<-SQL).first + object = Company.find_by_sql(<<~SQL).first SELECT c1.*, c2.type as string_value, c2.rating as int_value FROM companies c1, companies c2 WHERE c1.firm_id = c2.id @@ -705,6 +778,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase record.written_on = "Jan 01 00:00:00 2014" assert_equal record, YAML.load(YAML.dump(record)) end + ensure + # NOTE: Reset column info because global topics + # don't have tz-aware attributes by default. + Topic.reset_column_information end test "setting a time zone-aware time in the current time zone" do @@ -1004,13 +1081,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal ["title"], model.accessed_fields end - test "generated attribute methods ancestors have correct class" do + test "generated attribute methods ancestors have correct module" do mod = Topic.send(:generated_attribute_methods) - assert_match %r(GeneratedAttributeMethods), mod.inspect + assert_equal "Topic::GeneratedAttributeMethods", mod.inspect end private - def new_topic_like_ar_class(&block) klass = Class.new(ActiveRecord::Base) do self.table_name = "topics" diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 2632aec7ab..d6ac5a1057 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -56,6 +56,17 @@ module ActiveRecord assert_equal 255, UnoverloadedType.type_for_attribute("overloaded_string_with_limit").limit end + test "extra options are forwarded to the type caster constructor" do + klass = Class.new(OverloadedType) do + attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1 + end + + starts_at_type = klass.type_for_attribute(:starts_at) + assert_equal 3, starts_at_type.precision + assert_equal 2, starts_at_type.limit + assert_equal 1, starts_at_type.scale + end + test "nonexistent attribute" do data = OverloadedType.new(non_existent_decimal: 1) @@ -230,7 +241,7 @@ module ActiveRecord test "attributes not backed by database columns are always initialized" do OverloadedType.create! - model = OverloadedType.first + model = OverloadedType.last assert_nil model.non_existent_decimal model.non_existent_decimal = "123" @@ -242,7 +253,7 @@ module ActiveRecord attribute :non_existent_decimal, :decimal, default: 123 end child.create! - model = child.first + model = child.last assert_equal 123, model.non_existent_decimal end @@ -253,7 +264,7 @@ module ActiveRecord attribute :foo, :string, default: "lol" end child.create! - model = child.first + model = child.last assert_equal "lol", model.foo diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 88df0eed55..2d223a3035 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true require "cases/helper" +require "models/author" +require "models/book" require "models/bird" require "models/post" require "models/comment" @@ -38,7 +40,6 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def self.name; "Person"; end private - def should_be_cool unless first_name == "cool" errors.add :first_name, "not cool" @@ -81,7 +82,6 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase end private - def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) reflection = model.reflect_on_association(association_name) assert_no_difference "callbacks_for_model(#{model.name}).length" do @@ -643,10 +643,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_before_save company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } + new_client = assert_queries(0) { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" @@ -658,10 +655,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_before_save company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } + assert_queries(0) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } company.name += "-changed" assert_queries(3) { assert company.save } @@ -671,10 +665,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_via_block_before_save company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } + new_client = assert_queries(0) { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" @@ -686,10 +677,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_via_block_before_save company = companies(:first_firm) - # Load schema information so we don't query below if running just this test. - Client.define_attribute_methods - - assert_no_queries do + assert_queries(0) do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end @@ -1319,21 +1307,45 @@ end class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase self.use_transactional_tests = false unless supports_savepoints? - def setup - super + def create_member_with_organization organization = Organization.create - @member = Member.create - MemberDetail.create(organization: organization, member: @member) + member = Member.create + MemberDetail.create(organization: organization, member: member) + + member end def test_should_not_has_one_through_model - class << @member.organization + member = create_member_with_organization + + class << member.organization + def save(*args) + super + raise "Oh noes!" + end + end + assert_nothing_raised { member.save } + end + + def create_author_with_post_with_comment + Author.create! name: "David" # make comment_id not match author_id + author = Author.create! name: "Sergiy" + post = Post.create! author: author, title: "foo", body: "bar" + Comment.create! post: post, body: "cool comment" + + author + end + + def test_should_not_reversed_has_one_through_model + author = create_author_with_post_with_comment + + class << author.comment_on_first_post def save(*args) super raise "Oh noes!" end end - assert_nothing_raised { @member.save } + assert_nothing_raised { author.save } end end @@ -1660,6 +1672,10 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te super @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?") @pirate.birds.create(name: "cookoo") + + @author = Author.new(name: "DHH") + @author.published_books.build(name: "Rework", isbn: "1234") + @author.published_books.build(name: "Remote", isbn: "1234") end test "should automatically validate associations" do @@ -1668,6 +1684,42 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te assert_not_predicate @pirate, :valid? end + + test "rollbacks whole transaction and raises ActiveRecord::RecordInvalid when associations fail to #save! due to uniqueness validation failure" do + author_count_before_save = Author.count + book_count_before_save = Book.count + + assert_no_difference "Author.count" do + assert_no_difference "Book.count" do + exception = assert_raises(ActiveRecord::RecordInvalid) do + @author.save! + end + + assert_equal("Validation failed: Published books is invalid", exception.message) + end + end + + assert_equal(author_count_before_save, Author.count) + assert_equal(book_count_before_save, Book.count) + end + + test "rollbacks whole transaction when associations fail to #save due to uniqueness validation failure" do + author_count_before_save = Author.count + book_count_before_save = Book.count + + assert_no_difference "Author.count" do + assert_no_difference "Book.count" do + assert_nothing_raised do + result = @author.save + + assert_not(result) + end + end + end + + assert_equal(author_count_before_save, Author.count) + assert_equal(book_count_before_save, Book.count) + end end class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::TestCase @@ -1806,7 +1858,7 @@ class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::Te end class TestAutosaveAssociationOnAHasManyAssociationDefinedInSubclassWithAcceptsNestedAttributes < ActiveRecord::TestCase - def test_should_update_children_when_asssociation_redefined_in_subclass + def test_should_update_children_when_association_redefined_in_subclass agency = Agency.create!(name: "Agency") valid_project = Project.create!(firm: agency, name: "Initial") agency.update!( diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 09e5517449..1324bdf9b8 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -67,6 +67,16 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts + def test_generated_association_methods_module_name + mod = Post.send(:generated_association_methods) + assert_equal "Post::GeneratedAssociationMethods", mod.inspect + end + + def test_generated_relation_methods_module_name + mod = Post.send(:generated_relation_methods) + assert_equal "Post::GeneratedRelationMethods", mod.inspect + end + def test_column_names_are_escaped conn = ActiveRecord::Base.connection classname = conn.class.name[/[^:]*$/] @@ -438,12 +448,6 @@ class BasicsTest < ActiveRecord::TestCase Post.reset_table_name end - if current_adapter?(:Mysql2Adapter) - def test_update_all_with_order_and_limit - assert_equal 1, Topic.limit(1).order("id DESC").update_all(content: "bulk updated!") - end - end - def test_null_fields assert_nil Topic.find(1).parent_id assert_nil Topic.create("title" => "Hey you").parent_id @@ -691,6 +695,9 @@ class BasicsTest < ActiveRecord::TestCase topic = Topic.find(1) topic.attributes = attributes assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time + + topic.save! + assert_equal topic, Topic.find_by(attributes) end end @@ -1038,11 +1045,6 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_find_last - last = Developer.last - assert_equal last, Developer.all.merge!(order: "id desc").first - end - def test_last assert_equal Developer.all.merge!(order: "id desc").first, Developer.last end @@ -1058,23 +1060,23 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_ordered_last - last = Developer.all.merge!(order: "developers.salary ASC").last - assert_equal last, Developer.all.merge!(order: "developers.salary ASC").to_a.last + last = Developer.order("developers.salary ASC").last + assert_equal last, Developer.order("developers.salary": "ASC").to_a.last end def test_find_reverse_ordered_last - last = Developer.all.merge!(order: "developers.salary DESC").last - assert_equal last, Developer.all.merge!(order: "developers.salary DESC").to_a.last + last = Developer.order("developers.salary DESC").last + assert_equal last, Developer.order("developers.salary": "DESC").to_a.last end def test_find_multiple_ordered_last - last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last - assert_equal last, Developer.all.merge!(order: "developers.name, developers.salary DESC").to_a.last + last = Developer.order("developers.name, developers.salary DESC").last + assert_equal last, Developer.order(:"developers.name", "developers.salary": "DESC").to_a.last end def test_find_keeps_multiple_order_values - combined = Developer.all.merge!(order: "developers.name, developers.salary").to_a - assert_equal combined, Developer.all.merge!(order: ["developers.name", "developers.salary"]).to_a + combined = Developer.order("developers.name, developers.salary").to_a + assert_equal combined, Developer.order(:"developers.name", :"developers.salary").to_a end def test_find_keeps_multiple_group_values @@ -1139,11 +1141,14 @@ class BasicsTest < ActiveRecord::TestCase def test_clear_cache! # preheat cache c1 = Post.connection.schema_cache.columns("posts") + assert_not_equal 0, Post.connection.schema_cache.size + ActiveRecord::Base.clear_cache! + assert_equal 0, Post.connection.schema_cache.size + c2 = Post.connection.schema_cache.columns("posts") - c1.each_with_index do |v, i| - assert_not_same v, c2[i] - end + assert_not_equal 0, Post.connection.schema_cache.size + assert_equal c1, c2 end @@ -1213,6 +1218,8 @@ class BasicsTest < ActiveRecord::TestCase wr.close assert Marshal.load rd.read rd.close + ensure + self.class.send(:remove_const, "Post") if self.class.const_defined?("Post", false) end end @@ -1226,14 +1233,15 @@ class BasicsTest < ActiveRecord::TestCase end def test_attribute_names - assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"], - Company.attribute_names + expected = ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description", "metadata"] + assert_equal expected, Company.attribute_names end def test_has_attribute assert Company.has_attribute?("id") assert Company.has_attribute?("type") assert Company.has_attribute?("name") + assert Company.has_attribute?("metadata") assert_not Company.has_attribute?("lastname") assert_not Company.has_attribute?("age") end @@ -1407,6 +1415,14 @@ class BasicsTest < ActiveRecord::TestCase assert_not_includes SymbolIgnoredDeveloper.columns_hash.keys, "first_name" end + test ".columns_hash raises an error if the record has an empty table name" do + expected_message = "FirstAbstractClass has no table configured. Set one with FirstAbstractClass.table_name=" + exception = assert_raises(ActiveRecord::TableNotSpecified) do + FirstAbstractClass.columns_hash + end + assert_equal expected_message, exception.message + end + test "ignored columns have no attribute methods" do assert_not_respond_to Developer.new, :first_name assert_not_respond_to Developer.new, :first_name= @@ -1447,6 +1463,14 @@ class BasicsTest < ActiveRecord::TestCase assert_not_respond_to developer, :first_name= end + test "when ignored attribute is loaded, cast type should be preferred over DB type" do + developer = AttributedDeveloper.create + developer.update_column :name, "name" + + loaded_developer = AttributedDeveloper.where(id: developer.id).select("*").first + assert_equal "Developer: name", loaded_developer.name + end + test "ignored columns not included in SELECT" do query = Developer.all.to_sql.downcase @@ -1480,4 +1504,119 @@ class BasicsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.protected_environments = previous_protected_environments end + + test "creating a record raises if preventing writes" do + error = assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection_handler.while_preventing_writes do + Bird.create! name: "Bluejay" + end + end + + assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, error.message + end + + test "updating a record raises if preventing writes" do + bird = Bird.create! name: "Bluejay" + + error = assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection_handler.while_preventing_writes do + bird.update! name: "Robin" + end + end + + assert_match %r/\AWrite query attempted while in readonly mode: UPDATE /, error.message + end + + test "deleting a record raises if preventing writes" do + bird = Bird.create! name: "Bluejay" + + error = assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection_handler.while_preventing_writes do + bird.destroy! + end + end + + assert_match %r/\AWrite query attempted while in readonly mode: DELETE /, error.message + end + + test "selecting a record does not raise if preventing writes" do + bird = Bird.create! name: "Bluejay" + + ActiveRecord::Base.connection_handler.while_preventing_writes do + assert_equal bird, Bird.where(name: "Bluejay").first + end + end + + test "an explain query does not raise if preventing writes" do + Bird.create!(name: "Bluejay") + + ActiveRecord::Base.connection_handler.while_preventing_writes do + assert_queries(2) { Bird.where(name: "Bluejay").explain } + end + end + + test "an empty transaction does not raise if preventing writes" do + ActiveRecord::Base.connection_handler.while_preventing_writes do + assert_queries(2, ignore_none: true) do + Bird.transaction do + ActiveRecord::Base.connection.materialize_transactions + end + end + end + end + + test "preventing writes applies to all connections on a handler" do + conn1_error = assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection_handler.while_preventing_writes do + assert_equal ActiveRecord::Base.connection, Bird.connection + assert_not_equal ARUnit2Model.connection, Bird.connection + Bird.create!(name: "Bluejay") + end + end + + assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn1_error.message + + conn2_error = assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection_handler.while_preventing_writes do + assert_not_equal ActiveRecord::Base.connection, Professor.connection + assert_equal ARUnit2Model.connection, Professor.connection + Professor.create!(name: "Professor Bluejay") + end + end + + assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn2_error.message + end + + unless in_memory_db? + test "preventing writes with multiple handlers" do + ActiveRecord::Base.connects_to(database: { writing: :arunit, reading: :arunit }) + + conn1_error = assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connected_to(role: :writing) do + assert_equal :writing, ActiveRecord::Base.current_role + + ActiveRecord::Base.connection_handler.while_preventing_writes do + Bird.create!(name: "Bluejay") + end + end + end + + assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn1_error.message + + conn2_error = assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connected_to(role: :reading) do + assert_equal :reading, ActiveRecord::Base.current_role + + ActiveRecord::Base.connection_handler.while_preventing_writes do + Bird.create!(name: "Bluejay") + end + end + end + + assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, conn2_error.message + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + ActiveRecord::Base.establish_connection(:arunit) + end + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index d21218a997..0d0bf39f79 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -146,7 +146,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{Regexp.escape(c.quote_table_name("posts.id"))}/i) do Post.find_in_batches(batch_size: 1) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first @@ -430,7 +430,7 @@ class EachTest < ActiveRecord::TestCase assert_kind_of ActiveRecord::Relation, relation assert_kind_of Post, relation.first - relation = [not_a_post] * relation.count + [not_a_post] * relation.count end end end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index bd5f157ca1..720446b39d 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "models/topic" +require "models/reply" require "models/author" require "models/post" @@ -34,6 +35,77 @@ if ActiveRecord::Base.connection.prepared_statements ActiveSupport::Notifications.unsubscribe(@subscription) end + def test_statement_cache + @connection.clear_cache! + + topics = Topic.where(id: 1) + assert_equal [1], topics.map(&:id) + assert_includes statement_cache, to_sql_key(topics.arel) + + @connection.clear_cache! + + assert_not_includes statement_cache, to_sql_key(topics.arel) + end + + def test_statement_cache_with_query_cache + @connection.enable_query_cache! + @connection.clear_cache! + + topics = Topic.where(id: 1) + assert_equal [1], topics.map(&:id) + assert_includes statement_cache, to_sql_key(topics.arel) + ensure + @connection.disable_query_cache! + end + + def test_statement_cache_with_find + @connection.clear_cache! + + assert_equal 1, Topic.find(1).id + assert_raises(RecordNotFound) { SillyReply.find(2) } + + topic_sql = cached_statement(Topic, Topic.primary_key) + assert_includes statement_cache, to_sql_key(topic_sql) + + e = assert_raise { cached_statement(SillyReply, SillyReply.primary_key) } + assert_equal "SillyReply has no cached statement by \"id\"", e.message + + replies = SillyReply.where(id: 2).limit(1) + assert_includes statement_cache, to_sql_key(replies.arel) + end + + def test_statement_cache_with_find_by + @connection.clear_cache! + + assert_equal 1, Topic.find_by!(id: 1).id + assert_raises(RecordNotFound) { SillyReply.find_by!(id: 2) } + + topic_sql = cached_statement(Topic, [:id]) + assert_includes statement_cache, to_sql_key(topic_sql) + + e = assert_raise { cached_statement(SillyReply, [:id]) } + assert_equal "SillyReply has no cached statement by [:id]", e.message + + replies = SillyReply.where(id: 2).limit(1) + assert_includes statement_cache, to_sql_key(replies.arel) + end + + def test_statement_cache_with_in_clause + @connection.clear_cache! + + topics = Topic.where(id: [1, 3]).order(:id) + assert_equal [1, 3], topics.map(&:id) + assert_not_includes statement_cache, to_sql_key(topics.arel) + end + + def test_statement_cache_with_sql_string_literal + @connection.clear_cache! + + topics = Topic.where("topics.id = ?", 1) + assert_equal [1], topics.map(&:id) + assert_not_includes statement_cache, to_sql_key(topics.arel) + end + def test_too_many_binds bind_params_length = @connection.send(:bind_params_length) @@ -44,6 +116,19 @@ if ActiveRecord::Base.connection.prepared_statements assert_equal 0, topics.count end + def test_too_many_binds_with_query_cache + @connection.enable_query_cache! + + bind_params_length = @connection.send(:bind_params_length) + topics = Topic.where(id: (1 .. bind_params_length + 1).to_a) + assert_equal Topic.count, topics.count + + topics = Topic.where.not(id: (1 .. bind_params_length + 1).to_a) + assert_equal 0, topics.count + ensure + @connection.disable_query_cache! + end + def test_bind_from_join_in_subquery subquery = Author.joins(:thinking_posts).where(name: "David") scope = Author.from(subquery, "authors").where(id: 1) @@ -77,17 +162,29 @@ if ActiveRecord::Base.connection.prepared_statements assert_logs_binds(binds) end - def test_deprecate_supports_statement_cache - assert_deprecated { ActiveRecord::Base.connection.supports_statement_cache? } - end - private + def to_sql_key(arel) + sql = @connection.to_sql(arel) + @connection.respond_to?(:sql_key, true) ? @connection.send(:sql_key, sql) : sql + end + + def cached_statement(klass, key) + cache = klass.send(:cached_find_by_statement, key) do + raise "#{klass} has no cached statement by #{key.inspect}" + end + cache.send(:query_builder).instance_variable_get(:@sql) + end + + def statement_cache + @connection.instance_variable_get(:@statements).send(:cache) + end + def assert_logs_binds(binds) payload = { name: "SQL", sql: "select * from topics where id = ?", binds: binds, - type_casted_binds: @connection.type_casted_binds(binds) + type_casted_binds: @connection.send(:type_casted_binds, binds) } event = ActiveSupport::Notifications::Event.new( diff --git a/activerecord/test/cases/boolean_test.rb b/activerecord/test/cases/boolean_test.rb index ab9f974e2c..18824004d2 100644 --- a/activerecord/test/cases/boolean_test.rb +++ b/activerecord/test/cases/boolean_test.rb @@ -40,4 +40,13 @@ class BooleanTest < ActiveRecord::TestCase assert_equal b_false, Boolean.find_by(value: "false") assert_equal b_true, Boolean.find_by(value: "true") end + + def test_find_by_falsy_boolean_symbol + ActiveModel::Type::Boolean::FALSE_VALUES.each do |value| + b_false = Boolean.create!(value: value) + + assert_not_predicate b_false, :value? + assert_equal b_false, Boolean.find_by(id: b_false.id, value: value.to_s.to_sym) + end + end end diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 3a569f226e..c27eb8a65d 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -44,10 +44,88 @@ module ActiveRecord test "cache_key_with_version always has both key and version" do r1 = CacheMeWithVersion.create - assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.utc.to_s(:usec)}", r1.cache_key_with_version r2 = CacheMe.create - assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version + assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.utc.to_s(:usec)}", r2.cache_key_with_version + end + + test "cache_version is the same when it comes from the DB or from the user" do + skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_not_called(record_from_db, :updated_at) do + record_from_db.cache_version + end + + assert_equal record.cache_version, record_from_db.cache_version + end + + test "cache_version does not truncate zeros when timestamp ends in zeros" do + skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + + travel_to Time.now.beginning_of_day do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_not_called(record_from_db, :updated_at) do + record_from_db.cache_version + end + + assert_equal record.cache_version, record_from_db.cache_version + end + end + + test "cache_version calls updated_at when the value is generated at create time" do + record = CacheMeWithVersion.create + assert_called(record, :updated_at) do + record.cache_version + end + end + + test "cache_version does NOT call updated_at when value is from the database" do + skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_not_called(record_from_db, :updated_at) do + record_from_db.cache_version + end + end + + test "cache_version does call updated_at when it is assigned via a Time object" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_called(record_from_db, :updated_at) do + record_from_db.updated_at = Time.now + record_from_db.cache_version + end + end + + test "cache_version does call updated_at when it is assigned via a string" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_called(record_from_db, :updated_at) do + record_from_db.updated_at = Time.now.to_s + record_from_db.cache_version + end + end + + test "cache_version does call updated_at when it is assigned via a hash" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_called(record_from_db, :updated_at) do + record_from_db.updated_at = { 1 => 2016, 2 => 11, 3 => 12, 4 => 1, 5 => 2, 6 => 3, 7 => 22 } + record_from_db.cache_version + end + end + + test "updated_at on class but not on instance raises an error" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.where(id: record.id).select(:id).first + assert_raises(ActiveModel::MissingAttributeError) do + record_from_db.cache_version + end end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5c9ed42173..dbd1d03c4c 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -19,6 +19,7 @@ require "models/developer" require "models/post" require "models/comment" require "models/rating" +require "support/stubs/strong_parameters" class CalculationsTest < ActiveRecord::TestCase fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments @@ -138,6 +139,13 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_should_not_use_alias_for_grouped_field + assert_sql(/GROUP BY #{Regexp.escape(Account.connection.quote_table_name("accounts.firm_id"))}/i) do + c = Account.group(:firm_id).order("accounts_firm_id").sum(:credit_limit) + assert_equal [1, 2, 6, 9], c.keys.compact + end + end + def test_should_order_by_grouped_field c = Account.group(:firm_id).order("firm_id").sum(:credit_limit) assert_equal [1, 2, 6, 9], c.keys.compact @@ -184,7 +192,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_limit_is_kept return if current_adapter?(:OracleAdapter) - queries = assert_sql { Account.limit(1).count } + queries = capture_sql { Account.limit(1).count } assert_equal 1, queries.length assert_match(/LIMIT/, queries.first) end @@ -192,7 +200,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_offset_is_kept return if current_adapter?(:OracleAdapter) - queries = assert_sql { Account.offset(1).count } + queries = capture_sql { Account.offset(1).count } assert_equal 1, queries.length assert_match(/OFFSET/, queries.first) end @@ -200,14 +208,14 @@ class CalculationsTest < ActiveRecord::TestCase def test_limit_with_offset_is_kept return if current_adapter?(:OracleAdapter) - queries = assert_sql { Account.limit(1).offset(1).count } + queries = capture_sql { Account.limit(1).offset(1).count } assert_equal 1, queries.length assert_match(/LIMIT/, queries.first) assert_match(/OFFSET/, queries.first) end def test_no_limit_no_offset - queries = assert_sql { Account.count } + queries = capture_sql { Account.count } assert_equal 1, queries.length assert_no_match(/LIMIT/, queries.first) assert_no_match(/OFFSET/, queries.first) @@ -218,20 +226,17 @@ class CalculationsTest < ActiveRecord::TestCase Account.select("credit_limit, firm_name").count } - assert_match %r{accounts}i, e.message - assert_match "credit_limit, firm_name", e.message + assert_match %r{accounts}i, e.sql + assert_match "credit_limit, firm_name", e.sql end def test_apply_distinct_in_count - queries = assert_sql do + queries = capture_sql do Account.distinct.count Account.group(:firm_id).distinct.count end queries.each do |query| - # `table_alias_length` in `column_alias_for` would execute - # "SHOW max_identifier_length" statement in PostgreSQL adapter. - next if query == "SHOW max_identifier_length" assert_match %r{\ASELECT(?! DISTINCT) COUNT\(DISTINCT\b}, query end end @@ -242,6 +247,12 @@ class CalculationsTest < ActiveRecord::TestCase assert_queries(1) { assert_equal 11, posts.count(:all) } end + def test_count_with_eager_loading_and_custom_select_and_order + posts = Post.includes(:comments).order("comments.id").select(:type) + assert_queries(1) { assert_equal 11, posts.count } + assert_queries(1) { assert_equal 11, posts.count(:all) } + end + def test_count_with_eager_loading_and_custom_order_and_distinct posts = Post.includes(:comments).order("comments.id").distinct assert_queries(1) { assert_equal 11, posts.count } @@ -278,6 +289,18 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count end + def test_distinct_joins_count_with_group_by + expected = { nil => 4, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 } + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:author_id) + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count + assert_equal expected, Post.left_joins(:comments).group(:post_id).count("DISTINCT posts.author_id") + assert_equal expected, Post.left_joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count + + expected = { nil => 6, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 } + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:all) + assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count(:all) + end + def test_distinct_count_with_group_by_and_order_and_limit assert_equal({ 6 => 2 }, Account.group(:firm_id).distinct.order("1 DESC").limit(1).count) end @@ -344,6 +367,17 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 60, c[2] end + def test_should_calculate_grouped_with_longer_field + field = "a" * Account.connection.max_identifier_length + + Account.update_all("#{field} = credit_limit") + + c = Account.group(:firm_id).sum(field) + assert_equal 50, c[1] + assert_equal 105, c[6] + assert_equal 60, c[2] + end + def test_should_calculate_with_invalid_field assert_equal 6, Account.calculate(:count, "*") assert_equal 6, Account.calculate(:count, :all) @@ -428,11 +462,13 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_selected_field_with_include assert_equal 6, Account.includes(:firm).distinct.count assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count + assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT credit_limit") + assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT(credit_limit)") end def test_should_not_perform_joined_include_by_default assert_equal Account.count, Account.includes(:firm).count - queries = assert_sql { Account.includes(:firm).count } + queries = capture_sql { Account.includes(:firm).count } assert_no_match(/join/i, queries.last) end @@ -458,6 +494,24 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count end + def test_should_count_manual_select_with_count_all + assert_equal 5, Account.select("DISTINCT accounts.firm_id").count(:all) + end + + def test_should_count_with_manual_distinct_select_and_distinct + assert_equal 4, Account.select("DISTINCT accounts.firm_id").distinct(true).count + end + + def test_should_count_manual_select_with_group_with_count_all + expected = { nil => 1, 1 => 1, 2 => 1, 6 => 2, 9 => 1 } + actual = Account.select("DISTINCT accounts.firm_id").group("accounts.firm_id").count(:all) + assert_equal expected, actual + end + + def test_should_count_manual_with_count_all + assert_equal 6, Account.count(:all) + end + def test_count_selected_arel_attribute assert_equal 5, Account.select(Account.arel_table[:firm_id]).count assert_equal 4, Account.distinct.select(Account.arel_table[:firm_id]).count @@ -509,8 +563,10 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_count_field_of_root_table_with_conflicting_group_by_column - assert_equal({ 1 => 1 }, Firm.joins(:accounts).group(:firm_id).count) - assert_equal({ 1 => 1 }, Firm.joins(:accounts).group("accounts.firm_id").count) + expected = { 1 => 2, 2 => 1, 4 => 5, 5 => 2, 7 => 1 } + assert_equal expected, Post.joins(:comments).group(:post_id).count + assert_equal expected, Post.joins(:comments).group("comments.post_id").count + assert_equal expected, Post.joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count(:all) end def test_count_with_no_parameters_isnt_deprecated @@ -540,11 +596,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_sum_expression - if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter) - assert_equal 636, Account.sum("2 * credit_limit") - else - assert_equal 636, Account.sum("2 * credit_limit").to_i - end + assert_equal 636, Account.sum("2 * credit_limit") end def test_sum_expression_returns_zero_when_no_records_to_sum @@ -688,8 +740,9 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_not_auto_table_name_prefix_if_column_joined - Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)]) - assert_equal [7], Company.joins(:contracts).pluck(:developer_id) + company = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)]) + metadata = company.contracts.first.metadata + assert_equal [metadata], Company.joins(:contracts).pluck(:metadata) end def test_pluck_with_selection_clause @@ -717,6 +770,16 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id) end + def test_pluck_with_join + assert_equal [[2, 2], [4, 4]], Reply.includes(:topic).pluck(:id, :"topics.id") + end + + def test_group_by_with_order_by_virtual_count_attribute + expected = { "SpecialPost" => 1, "StiPost" => 2 } + actual = Post.group(:type).order(:count).limit(2).maximum(:comments_count) + assert_equal expected, actual + end if current_adapter?(:PostgreSQLAdapter) + def test_group_by_with_limit expected = { "Post" => 8, "SpecialPost" => 1 } actual = Post.includes(:comments).group(:type).order(:type).limit(2).count("comments.id") @@ -783,7 +846,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_columns_with_same_name expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]] - actual = Topic.joins(:replies) + actual = Topic.joins(:replies).order(:id) .pluck("topics.title", "replies_topics.title") assert_equal expected, actual end @@ -803,28 +866,25 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_loaded_relation - Company.attribute_names # Load schema information so we don't query below companies = Company.order(:id).limit(3).load - assert_no_queries do + assert_queries(0) do assert_equal ["37signals", "Summit", "Microsoft"], companies.pluck(:name) end end def test_pluck_loaded_relation_multiple_columns - Company.attribute_names # Load schema information so we don't query below companies = Company.order(:id).limit(3).load - assert_no_queries do + assert_queries(0) do assert_equal [[1, "37signals"], [2, "Summit"], [3, "Microsoft"]], companies.pluck(:id, :name) end end def test_pluck_loaded_relation_sql_fragment - Company.attribute_names # Load schema information so we don't query below companies = Company.order(:name).limit(3).load - assert_queries 1 do + assert_queries(1) do assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck(Arel.sql("DISTINCT name")) end end @@ -832,13 +892,13 @@ class CalculationsTest < ActiveRecord::TestCase def test_pick_one assert_equal "The First Topic", Topic.order(:id).pick(:heading) assert_nil Topic.none.pick(:heading) - assert_nil Topic.where("1=0").pick(:heading) + assert_nil Topic.where(id: 9999999999999999999).pick(:heading) end def test_pick_two assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address) assert_nil Topic.none.pick(:author_name, :author_email_address) - assert_nil Topic.where("1=0").pick(:author_name, :author_email_address) + assert_nil Topic.where(id: 9999999999999999999).pick(:author_name, :author_email_address) end def test_pick_delegate_to_all @@ -876,26 +936,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_having_with_strong_parameters - protected_params = Class.new do - attr_reader :permitted - alias :permitted? :permitted - - def initialize(parameters) - @parameters = parameters - @permitted = false - end - - def to_h - @parameters - end - - def permit! - @permitted = true - self - end - end - - params = protected_params.new(credit_limit: "50") + params = ProtectedParams.new(credit_limit: "50") assert_raises(ActiveModel::ForbiddenAttributesError) do Account.group(:id).having(params) @@ -911,15 +952,15 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end - def test_deprecate_count_with_block_and_column_name - assert_deprecated do - assert_equal 6, Account.count(:firm_id) { true } + def test_count_with_block_and_column_name_raises_an_error + assert_raises(ArgumentError) do + Account.count(:firm_id) { true } end end - def test_deprecate_sum_with_block_and_column_name - assert_deprecated do - assert_equal 6, Account.sum(:firm_id) { 1 } + def test_sum_with_block_and_column_name_raises_an_error + assert_raises(ArgumentError) do + Account.sum(:firm_id) { 1 } end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 0ea3fb86a6..b4026078f1 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -21,7 +21,7 @@ class CallbackDeveloper < ActiveRecord::Base def callback_object(callback_method) klass = Class.new - klass.send(:define_method, callback_method) do |model| + klass.define_method(callback_method) do |model| model.history << [callback_method, :object] end klass.new @@ -458,10 +458,6 @@ class CallbacksTest < ActiveRecord::TestCase [ :before_validation, :object ], [ :before_validation, :block ], [ :before_validation, :throwing_abort ], - [ :after_rollback, :block ], - [ :after_rollback, :object ], - [ :after_rollback, :proc ], - [ :after_rollback, :method ], ], david.history end diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 483383257b..f07f3c42e6 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -171,5 +171,39 @@ module ActiveRecord assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end + + test "cache_key should be stable when using collection_cache_versioning" do + with_collection_cache_versioning do + developers = Developer.where(salary: 100000) + + assert_match(/\Adevelopers\/query-(\h+)\z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)\z/ =~ developers.cache_key + + assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1 + end + end + + test "cache_version for relation" do + with_collection_cache_versioning do + developers = Developer.where(salary: 100000).order(updated_at: :desc) + last_developer_timestamp = developers.first.updated_at + + assert_match(/(\d+)-(\d+)\z/, developers.cache_version) + + /(\d+)-(\d+)\z/ =~ developers.cache_version + + assert_equal developers.count.to_s, $1 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $2 + end + end + + def with_collection_cache_versioning(value = true) + @old_collection_cache_versioning = ActiveRecord::Base.collection_cache_versioning + ActiveRecord::Base.collection_cache_versioning = value + yield + ensure + ActiveRecord::Base.collection_cache_versioning = @old_collection_cache_versioning + end end end diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index 584e03d196..25e2f20676 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -14,6 +14,9 @@ if ActiveRecord::Base.connection.supports_comments? class BlankComment < ActiveRecord::Base end + class PkCommented < ActiveRecord::Base + end + setup do @connection = ActiveRecord::Base.connection @@ -35,8 +38,13 @@ if ActiveRecord::Base.connection.supports_comments? t.index :absent_comment end + @connection.create_table("pk_commenteds", comment: "Table comment", id: false, force: true) do |t| + t.integer :id, comment: "Primary key comment", primary_key: true + end + Commented.reset_column_information BlankComment.reset_column_information + PkCommented.reset_column_information end teardown do @@ -44,6 +52,11 @@ if ActiveRecord::Base.connection.supports_comments? @connection.drop_table "blank_comments", if_exists: true end + def test_default_primary_key_comment + column = Commented.columns_hash["id"] + assert_nil column.comment + end + def test_column_created_in_block column = Commented.columns_hash["name"] assert_equal :string, column.type @@ -164,5 +177,17 @@ if ActiveRecord::Base.connection.supports_comments? column = Commented.columns_hash["name"] assert_nil column.comment end + + def test_comment_on_primary_key + column = PkCommented.columns_hash["id"] + assert_equal "Primary key comment", column.comment + assert_equal "Table comment", @connection.table_comment("pk_commenteds") + end + + def test_schema_dump_with_primary_key_comment + output = dump_table_schema "pk_commenteds" + assert_match %r[create_table "pk_commenteds",.*\s+comment: "Table comment"], output + assert_no_match %r[create_table "pk_commenteds",.*\s+comment: "Primary key comment"], output + end end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 51d0cc3d12..843242a897 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -29,7 +29,7 @@ module ActiveRecord def test_establish_connection_uses_spec_name old_config = ActiveRecord::Base.configurations - config = { "readonly" => { "adapter" => "sqlite3" } } + config = { "readonly" => { "adapter" => "sqlite3", "pool" => "5" } } ActiveRecord::Base.configurations = config resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations) spec = resolver.spec(:readonly) @@ -367,11 +367,24 @@ module ActiveRecord assert_same klass2.connection, ActiveRecord::Base.connection end + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end + + class MyClass < ApplicationRecord + end + def test_connection_specification_name_should_fallback_to_parent klassA = Class.new(Base) klassB = Class.new(klassA) + klassC = Class.new(MyClass) assert_equal klassB.connection_specification_name, klassA.connection_specification_name + assert_equal klassC.connection_specification_name, klassA.connection_specification_name + + assert_equal "primary", klassA.connection_specification_name + assert_equal "primary", klassC.connection_specification_name + klassA.connection_specification_name = "readonly" assert_equal "readonly", klassB.connection_specification_name end @@ -382,6 +395,11 @@ module ActiveRecord assert_not_nil ActiveRecord::Base.connection assert_same klass2.connection, ActiveRecord::Base.connection end + + def test_default_handlers_are_writing_and_reading + assert_equal :writing, ActiveRecord::Base.writing_role + assert_equal :reading, ActiveRecord::Base.reading_role + end end end end diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index a394430dfe..d3184f39f5 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -108,11 +108,17 @@ module ActiveRecord ActiveRecord::Base.connected_to(role: :reading) do @ro_handler = ActiveRecord::Base.connection_handler assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading] + assert_equal :reading, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :reading) + assert_not ActiveRecord::Base.connected_to?(role: :writing) end ActiveRecord::Base.connected_to(role: :writing) do assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) + assert_not ActiveRecord::Base.connected_to?(role: :reading) end ensure ActiveRecord::Base.configurations = @prev_configs @@ -120,11 +126,38 @@ module ActiveRecord ENV["RAILS_ENV"] = previous_env end + def test_establish_connection_using_3_levels_config_with_non_default_handlers + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { default: :primary, readonly: :readonly }) + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:default].retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:readonly].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + def test_switching_connections_with_database_url previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo" ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) + handler = ActiveRecord::Base.connection_handler assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] @@ -142,6 +175,9 @@ module ActiveRecord config = { adapter: "sqlite3", database: "db/readonly.sqlite3" } ActiveRecord::Base.connected_to(database: { writing: config }) do + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) + handler = ActiveRecord::Base.connection_handler assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] @@ -167,23 +203,53 @@ module ActiveRecord assert_equal "must provide a `database` or a `role`.", error.message end - def test_switching_connections_with_database_symbol + def test_switching_connections_with_database_symbol_uses_default_role + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "animals" => { adapter: "sqlite3", database: "db/animals.sqlite3" }, + "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connected_to(database: :animals) do + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) + + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config["default_env"]["animals"], pool.spec.config) + end + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_with_database_hash_uses_passed_role_and_database previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" config = { "default_env" => { - "readonly" => { adapter: "sqlite3", database: "db/readonly.sqlite3" }, - "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" } + "animals" => { adapter: "sqlite3", database: "db/animals.sqlite3" }, + "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" } } } @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config - ActiveRecord::Base.connected_to(database: :readonly) do + ActiveRecord::Base.connected_to(database: { writing: :primary }) do + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) + handler = ActiveRecord::Base.connection_handler - assert_equal handler, ActiveRecord::Base.connection_handlers[:readonly] + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] assert_not_nil pool = handler.retrieve_connection_pool("primary") - assert_equal(config["default_env"]["readonly"], pool.spec.config) + assert_equal(config["default_env"]["primary"], pool.spec.config) end ensure ActiveRecord::Base.configurations = @prev_configs @@ -201,6 +267,8 @@ module ActiveRecord assert_equal 1, ActiveRecord::Base.connection_handlers.size assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + assert_equal :writing, ActiveRecord::Base.current_role + assert ActiveRecord::Base.connected_to?(role: :writing) ensure ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.establish_connection(:arunit) @@ -280,6 +348,71 @@ module ActiveRecord assert_nil @rw_handler.retrieve_connection_pool("foo") assert_nil @ro_handler.retrieve_connection_pool("foo") end + + def test_connection_handlers_are_per_thread_and_not_per_fiber + original_handlers = ActiveRecord::Base.connection_handlers + + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new } + + reading_handler = ActiveRecord::Base.connection_handlers[:reading] + + reading = ActiveRecord::Base.with_handler(:reading) do + Person.connection_handler + end + + assert_not_equal reading, ActiveRecord::Base.connection_handler + assert_equal reading, reading_handler + ensure + ActiveRecord::Base.connection_handlers = original_handlers + end + + def test_connection_handlers_swapping_connections_in_fiber + original_handlers = ActiveRecord::Base.connection_handlers + + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new } + + reading_handler = ActiveRecord::Base.connection_handlers[:reading] + + enum = Enumerator.new do |r| + r << ActiveRecord::Base.connection_handler + end + + reading = ActiveRecord::Base.with_handler(:reading) do + enum.next + end + + assert_equal reading, reading_handler + ensure + ActiveRecord::Base.connection_handlers = original_handlers + end + + def test_calling_connected_to_on_a_non_existent_handler_raises + error = assert_raises ActiveRecord::ConnectionNotEstablished do + ActiveRecord::Base.connected_to(role: :reading) do + Person.first + end + end + + assert_equal "No connection pool with 'primary' found for the 'reading' role.", error.message + end + + def test_default_handlers_are_writing_and_reading + assert_equal :writing, ActiveRecord::Base.writing_role + assert_equal :reading, ActiveRecord::Base.reading_role + end + + def test_an_application_can_change_the_default_handlers + old_writing = ActiveRecord::Base.writing_role + old_reading = ActiveRecord::Base.reading_role + ActiveRecord::Base.writing_role = :default + ActiveRecord::Base.reading_role = :readonly + + assert_equal :default, ActiveRecord::Base.writing_role + assert_equal :readonly, ActiveRecord::Base.reading_role + ensure + ActiveRecord::Base.writing_role = old_writing + ActiveRecord::Base.reading_role = old_reading + end end end end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 06c1c51724..6372abbf3f 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -46,6 +46,14 @@ module ActiveRecord assert_equal expected, actual end + def test_resolver_with_nil_database_url_and_current_env + ENV["RAILS_ENV"] = "foo" + config = { "foo" => { "adapter" => "postgres", "url" => ENV["DATABASE_URL"] } } + actual = resolve_spec(:foo, config) + expected = { "adapter" => "postgres", "url" => nil, "name" => "foo" } + assert_equal expected, actual + end + def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env ENV["DATABASE_URL"] = "postgres://localhost/foo" ENV["RACK_ENV"] = "foo" @@ -64,6 +72,16 @@ module ActiveRecord assert_equal expected, actual end + def test_resolver_with_database_uri_and_multiple_envs + ENV["DATABASE_URL"] = "postgres://localhost" + ENV["RAILS_ENV"] = "test" + + config = { "production" => { "adapter" => "postgresql", "database" => "foo_prod" }, "test" => { "adapter" => "postgresql", "database" => "foo_test" } } + actual = resolve_spec(:test, config) + expected = { "adapter" => "postgresql", "database" => "foo_test", "host" => "localhost", "name" => "test" } + assert_equal expected, actual + end + def test_resolver_with_database_uri_and_unknown_symbol_key ENV["DATABASE_URL"] = "postgres://localhost/foo" config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } @@ -226,6 +244,25 @@ module ActiveRecord assert_equal expected, actual end + def test_no_url_sub_key_with_database_url_doesnt_trample_other_envs + ENV["DATABASE_URL"] = "postgres://localhost/baz" + + config = { "default_env" => { "database" => "foo" }, "other_env" => { "url" => "postgres://foohost/bardb" } } + actual = resolve_config(config) + expected = { "default_env" => + { "database" => "baz", + "adapter" => "postgresql", + "host" => "localhost" + }, + "other_env" => + { "adapter" => "postgresql", + "database" => "bardb", + "host" => "foohost" + } + } + assert_equal expected, actual + end + def test_merge_no_conflicts_with_database_url ENV["DATABASE_URL"] = "postgres://localhost/foo" @@ -255,6 +292,37 @@ module ActiveRecord } assert_equal expected, actual end + + def test_merge_no_conflicts_with_database_url_and_adapter + ENV["DATABASE_URL"] = "postgres://localhost/foo" + + config = { "default_env" => { "adapter" => "postgresql", "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + + def test_merge_no_conflicts_with_database_url_and_numeric_pool + ENV["DATABASE_URL"] = "postgres://localhost/foo" + + config = { "default_env" => { "pool" => 5 } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => 5 + } + } + + assert_equal expected, actual + end end end end diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index 02e76ce146..774380d7e0 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -27,8 +27,12 @@ if current_adapter?(:Mysql2Adapter) def test_string_types assert_lookup_type :string, "enum('one', 'two', 'three')" assert_lookup_type :string, "ENUM('one', 'two', 'three')" + assert_lookup_type :string, "enum ('one', 'two', 'three')" + assert_lookup_type :string, "ENUM ('one', 'two', 'three')" assert_lookup_type :string, "set('one', 'two', 'three')" assert_lookup_type :string, "SET('one', 'two', 'three')" + assert_lookup_type :string, "set ('one', 'two', 'three')" + assert_lookup_type :string, "SET ('one', 'two', 'three')" end def test_set_type_with_value_matching_other_type @@ -36,7 +40,7 @@ if current_adapter?(:Mysql2Adapter) end def test_enum_type_with_value_matching_other_type - assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')" + assert_lookup_type :string, "ENUM('unicode', '8bit', 'none', 'time')" end def test_binary_types @@ -54,7 +58,6 @@ if current_adapter?(:Mysql2Adapter) end private - def assert_lookup_type(type, lookup) cast_type = @connection.send(:type_map).lookup(lookup) assert_equal type, cast_type.type diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 67496381d1..28e232b88f 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -6,8 +6,9 @@ module ActiveRecord module ConnectionAdapters class SchemaCacheTest < ActiveRecord::TestCase def setup - connection = ActiveRecord::Base.connection - @cache = SchemaCache.new connection + @connection = ActiveRecord::Base.connection + @cache = SchemaCache.new @connection + @database_version = @connection.get_database_version end def test_primary_key @@ -19,6 +20,7 @@ module ActiveRecord @cache.columns_hash("posts") @cache.data_sources("posts") @cache.primary_keys("posts") + @cache.indexes("posts") new_cache = YAML.load(YAML.dump(@cache)) assert_no_queries do @@ -26,19 +28,47 @@ module ActiveRecord assert_equal 12, new_cache.columns_hash("posts").size assert new_cache.data_sources("posts") assert_equal "id", new_cache.primary_keys("posts") + assert_equal 1, new_cache.indexes("posts").size + assert_equal @database_version.to_s, new_cache.database_version.to_s end end def test_yaml_loads_5_1_dump - body = File.open(schema_dump_path).read - cache = YAML.load(body) + @cache = YAML.load(File.read(schema_dump_path)) assert_no_queries do - assert_equal 11, cache.columns("posts").size - assert_equal 11, cache.columns_hash("posts").size - assert cache.data_sources("posts") - assert_equal "id", cache.primary_keys("posts") + assert_equal 11, @cache.columns("posts").size + assert_equal 11, @cache.columns_hash("posts").size + assert @cache.data_sources("posts") + assert_equal "id", @cache.primary_keys("posts") + end + end + + def test_yaml_loads_5_1_dump_without_indexes_still_queries_for_indexes + @cache = YAML.load(File.read(schema_dump_path)) + + # Simulate assignment in railtie after loading the cache. + old_cache, @connection.schema_cache = @connection.schema_cache, @cache + + assert_queries :any, ignore_none: true do + assert_equal 1, @cache.indexes("posts").size end + ensure + @connection.schema_cache = old_cache + end + + def test_yaml_loads_5_1_dump_without_database_version_still_queries_for_database_version + @cache = YAML.load(File.read(schema_dump_path)) + + # Simulate assignment in railtie after loading the cache. + old_cache, @connection.schema_cache = @connection.schema_cache, @cache + + # We can't verify queries get executed because the database version gets + # cached in both MySQL and PostgreSQL outside of the schema cache. + assert_nil @cache.instance_variable_get(:@database_version) + assert_equal @database_version.to_s, @cache.database_version.to_s + ensure + @connection.schema_cache = old_cache end def test_primary_key_for_non_existent_table @@ -55,15 +85,34 @@ module ActiveRecord assert_equal columns_hash, @cache.columns_hash("posts") end + def test_caches_indexes + indexes = @cache.indexes("posts") + assert_equal indexes, @cache.indexes("posts") + end + + def test_caches_database_version + @cache.database_version # cache database_version + + assert_no_queries do + assert_equal @database_version.to_s, @cache.database_version.to_s + + if current_adapter?(:Mysql2Adapter) + assert_not_nil @cache.database_version.full_version_string + end + end + end + def test_clearing @cache.columns("posts") @cache.columns_hash("posts") @cache.data_sources("posts") @cache.primary_keys("posts") + @cache.indexes("posts") @cache.clear! assert_equal 0, @cache.size + assert_nil @cache.instance_variable_get(:@database_version) end def test_dump_and_load @@ -71,6 +120,7 @@ module ActiveRecord @cache.columns_hash("posts") @cache.data_sources("posts") @cache.primary_keys("posts") + @cache.indexes("posts") @cache = Marshal.load(Marshal.dump(@cache)) @@ -79,6 +129,8 @@ module ActiveRecord assert_equal 12, @cache.columns_hash("posts").size assert @cache.data_sources("posts") assert_equal "id", @cache.primary_keys("posts") + assert_equal 1, @cache.indexes("posts").size + assert_equal @database_version.to_s, @cache.database_version.to_s end end @@ -91,8 +143,23 @@ module ActiveRecord @cache.clear_data_source_cache!("posts") end - private + test "#columns_hash? is populated by #columns_hash" do + assert_not @cache.columns_hash?("posts") + + @cache.columns_hash("posts") + assert @cache.columns_hash?("posts") + end + + test "#columns_hash? is not populated by #data_source_exists?" do + assert_not @cache.columns_hash?("posts") + + @cache.data_source_exists?("posts") + + assert_not @cache.columns_hash?("posts") + end + + private def schema_dump_path "test/assets/schema_dump_5_1.yml" end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 1c79d776f0..e92bb40632 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -109,7 +109,6 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end private - def assert_lookup_type(type, lookup) cast_type = @connection.send(:type_map).lookup(lookup) assert_equal type, cast_type.type diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 633d56e479..ccbb6e16cd 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -507,7 +507,6 @@ module ActiveRecord pool.schema_cache = schema_cache pool.with_connection do |conn| - assert_not_same pool.schema_cache, conn.schema_cache assert_equal pool.schema_cache.size, conn.schema_cache.size assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts) end @@ -567,23 +566,21 @@ module ActiveRecord def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| - begin - thread = timed_join_result = nil - @pool.with_connection do |connection| - thread = Thread.new { @pool.send(group_action_method) } - - # give the other `thread` some time to get stuck in `group_action_method` - timed_join_result = thread.join(0.3) - # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to - # release our connection - assert_nil timed_join_result - - # assert that since this is within default timeout our connection hasn't been forcefully taken away from us - assert_predicate @pool, :active_connection? - end - ensure - thread.join if thread && !timed_join_result # clean up the other thread + thread = timed_join_result = nil + @pool.with_connection do |connection| + thread = Thread.new { @pool.send(group_action_method) } + + # give the other `thread` some time to get stuck in `group_action_method` + timed_join_result = thread.join(0.3) + # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to + # release our connection + assert_nil timed_join_result + + # assert that since this is within default timeout our connection hasn't been forcefully taken away from us + assert_predicate @pool, :active_connection? end + ensure + thread.join if thread && !timed_join_result # clean up the other thread end end @@ -697,6 +694,28 @@ module ActiveRecord end end + def test_public_connections_access_threadsafe + _conn1 = @pool.checkout + conn2 = @pool.checkout + + connections = @pool.connections + found_conn = nil + + # Without assuming too much about implementation + # details make sure that a concurrent change to + # the pool is thread-safe. + connections.each_index do |idx| + if connections[idx] == conn2 + Thread.new do + @pool.remove(conn2) + end.join + end + found_conn = connections[idx] + end + + assert_not_nil found_conn + end + private def with_single_connection_pool one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 99d286dc52..cc4f86a0fb 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -144,7 +144,7 @@ class CounterCacheTest < ActiveRecord::TestCase test "update other counters on parent destroy" do david, joanna = dog_lovers(:david, :joanna) - joanna = joanna # squelch a warning + _ = joanna # squelch a warning assert_difference "joanna.reload.dogs_count", -1 do david.destroy diff --git a/activerecord/test/cases/database_configurations_test.rb b/activerecord/test/cases/database_configurations_test.rb new file mode 100644 index 0000000000..ed8151f01a --- /dev/null +++ b/activerecord/test/cases/database_configurations_test.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require "cases/helper" + +class DatabaseConfigurationsTest < ActiveRecord::TestCase + unless in_memory_db? + def test_empty_returns_true_when_db_configs_are_empty + old_config = ActiveRecord::Base.configurations + config = {} + + ActiveRecord::Base.configurations = config + + assert_predicate ActiveRecord::Base.configurations, :empty? + assert_predicate ActiveRecord::Base.configurations, :blank? + ensure + ActiveRecord::Base.configurations = old_config + ActiveRecord::Base.establish_connection :arunit + end + end + + def test_configs_for_getter_with_env_name + configs = ActiveRecord::Base.configurations.configs_for(env_name: "arunit") + + assert_equal 1, configs.size + assert_equal ["arunit"], configs.map(&:env_name) + end + + def test_configs_for_getter_with_env_and_spec_name + config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", spec_name: "primary") + + assert_equal "arunit", config.env_name + assert_equal "primary", config.spec_name + end + + def test_default_hash_returns_config_hash_from_default_env + original_rails_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "arunit" + + assert_equal ActiveRecord::Base.configurations.configs_for(env_name: "arunit", spec_name: "primary").config, ActiveRecord::Base.configurations.default_hash + ensure + ENV["RAILS_ENV"] = original_rails_env + end + + def test_find_db_config_returns_a_db_config_object_for_the_given_env + config = ActiveRecord::Base.configurations.find_db_config("arunit2") + + assert_equal "arunit2", config.env_name + assert_equal "primary", config.spec_name + end + + def test_to_h_turns_db_config_object_back_into_a_hash + configs = ActiveRecord::Base.configurations + assert_equal "ActiveRecord::DatabaseConfigurations", configs.class.name + assert_equal "Hash", configs.to_h.class.name + assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"], ActiveRecord::Base.configurations.to_h.keys.sort + end +end + +class LegacyDatabaseConfigurationsTest < ActiveRecord::TestCase + unless in_memory_db? + def test_setting_configurations_hash + old_config = ActiveRecord::Base.configurations + config = { "adapter" => "sqlite3" } + + assert_deprecated do + ActiveRecord::Base.configurations["readonly"] = config + end + + assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements", "readonly"], ActiveRecord::Base.configurations.configs_for.map(&:env_name).sort + ensure + ActiveRecord::Base.configurations = old_config + ActiveRecord::Base.establish_connection :arunit + end + end + + def test_can_turn_configurations_into_a_hash + assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not." + assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort + end + + def test_each_is_deprecated + assert_deprecated do + ActiveRecord::Base.configurations.each do |db_config| + assert_equal "primary", db_config.spec_name + end + end + end + + def test_first_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_fetch_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.fetch("arunit").first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_values_are_deprecated + config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config) + assert_deprecated do + assert_equal config_hashes, ActiveRecord::Base.configurations.values + end + end + + def test_unsupported_method_raises + assert_raises NotImplementedError do + ActiveRecord::Base.configurations.select { |a| a == "foo" } + end + end +end diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb new file mode 100644 index 0000000000..fd02d2acb4 --- /dev/null +++ b/activerecord/test/cases/database_selector_test.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/person" +require "action_dispatch" + +module ActiveRecord + class DatabaseSelectorTest < ActiveRecord::TestCase + setup do + @session_store = {} + @session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store) + end + + teardown do + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + + def test_empty_session + assert_equal Time.at(0), @session.last_write_timestamp + end + + def test_writing_the_session_timestamps + assert @session.update_last_write_timestamp + + session2 = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store) + assert_equal @session.last_write_timestamp, session2.last_write_timestamp + end + + def test_writing_session_time_changes + assert @session.update_last_write_timestamp + + before = @session.last_write_timestamp + sleep(0.1) + + assert @session.update_last_write_timestamp + assert_not_equal before, @session.last_write_timestamp + end + + def test_read_from_replicas + @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds) + + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + called = false + resolver.read do + called = true + assert ActiveRecord::Base.connected_to?(role: :reading) + end + assert called + end + + def test_read_from_primary + @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now) + + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + called = false + resolver.read do + called = true + assert ActiveRecord::Base.connected_to?(role: :writing) + end + assert called + end + + def test_write_to_primary + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + end + + def test_write_to_primary_with_exception + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + assert_raises(ActiveRecord::RecordNotFound) do + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + raise ActiveRecord::RecordNotFound + end + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + end + + def test_read_from_primary_with_options + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + + read = false + resolver.read do + assert ActiveRecord::Base.connected_to?(role: :writing) + read = true + end + assert read + end + + def test_read_from_replica_with_no_delay + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.seconds) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + + read = false + resolver.read do + assert ActiveRecord::Base.connected_to?(role: :reading) + read = true + end + assert read + end + + def test_the_middleware_chooses_writing_role_with_POST_request + middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env| + assert ActiveRecord::Base.connected_to?(role: :writing) + [200, {}, ["body"]] + }) + assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST") + end + + def test_the_middleware_chooses_reading_role_with_GET_request + middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env| + assert ActiveRecord::Base.connected_to?(role: :reading) + [200, {}, ["body"]] + }) + assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET") + end + end +end diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index 1c934602ec..119d48b85e 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -23,7 +23,6 @@ class DatabaseStatementsTest < ActiveRecord::TestCase end private - def return_the_inserted_id(method:) # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if current_adapter?(:OracleAdapter) diff --git a/activerecord/test/cases/date_test.rb b/activerecord/test/cases/date_test.rb index 9f412cdb63..2475d4a34b 100644 --- a/activerecord/test/cases/date_test.rb +++ b/activerecord/test/cases/date_test.rb @@ -23,23 +23,13 @@ class DateTest < ActiveRecord::TestCase valid_dates.each do |date_src| topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s) - # Oracle DATE columns are datetime columns and Oracle adapter returns Time value - if current_adapter?(:OracleAdapter) - assert_equal(topic.last_read.to_date, Date.new(*date_src)) - else - assert_equal(topic.last_read, Date.new(*date_src)) - end + assert_equal(topic.last_read, Date.new(*date_src)) end invalid_dates.each do |date_src| assert_nothing_raised do topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s) - # Oracle DATE columns are datetime columns and Oracle adapter returns Time value - if current_adapter?(:OracleAdapter) - assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object") - else - assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object") - end + assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object") end end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index e64a8372d0..79d63949ca 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -29,7 +29,7 @@ if subsecond_precision_supported? def test_datetime_precision_is_truncated_on_assignment @connection.create_table(:foos, force: true) - @connection.add_column :foos, :created_at, :datetime, precision: 0 + @connection.add_column :foos, :created_at, :datetime, precision: 0 @connection.add_column :foos, :updated_at, :datetime, precision: 6 time = ::Time.now.change(nsec: 123456789) @@ -45,6 +45,26 @@ if subsecond_precision_supported? assert_equal 123456000, foo.updated_at.nsec end + unless current_adapter?(:Mysql2Adapter) + def test_no_datetime_precision_isnt_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :created_at, :datetime + @connection.add_column :foos, :updated_at, :datetime, precision: 6 + + time = ::Time.now.change(nsec: 123) + foo = Foo.new(created_at: time, updated_at: time) + + assert_equal 123, foo.created_at.nsec + assert_equal 0, foo.updated_at.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.created_at.nsec + assert_equal 0, foo.updated_at.nsec + end + end + def test_timestamps_helper_with_custom_precision @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 @@ -62,7 +82,7 @@ if subsecond_precision_supported? end def test_invalid_datetime_precision_raises_error - assert_raises ActiveRecord::ActiveRecordError do + assert_raises ArgumentError do @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 7 end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 5d02e59ef6..50a86b0a19 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -89,7 +89,7 @@ if current_adapter?(:PostgreSQLAdapter) test "schema dump includes default expression" do output = dump_table_schema("defaults") - if ActiveRecord::Base.connection.postgresql_version >= 100000 + if ActiveRecord::Base.connection.database_version >= 100000 assert_match %r/t\.date\s+"modified_date",\s+default: -> { "CURRENT_DATE" }/, output assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "CURRENT_TIMESTAMP" }/, output else diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index dfd74bfcb4..a2a501a794 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -352,7 +352,7 @@ class DirtyTest < ActiveRecord::TestCase Person.where(id: person.id).update_all(first_name: "baz") end - old_lock_version = person.lock_version + old_lock_version = person.lock_version + 1 with_partial_writes Person, true do assert_no_queries { 2.times { person.save! } } diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 867ce7082b..ae0ce195b3 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -44,6 +44,11 @@ class EnumTest < ActiveRecord::TestCase assert_equal books(:rfr), authors(:david).unpublished_books.first end + test "find via negative scope" do + assert Book.not_published.exclude?(@book) + assert Book.not_proposed.include?(@book) + end + test "find via where with values" do published, written = Book.statuses[:published], Book.statuses[:written] @@ -438,6 +443,20 @@ class EnumTest < ActiveRecord::TestCase assert_equal ["drafted", "uploaded"], book2.status_change end + test "attempting to modify enum raises error" do + e = assert_raises(RuntimeError) do + Book.statuses["bad_enum"] = 40 + end + + assert_match(/can't modify frozen/, e.message) + + e = assert_raises(RuntimeError) do + Book.statuses.delete("published") + end + + assert_match(/can't modify frozen/, e.message) + end + test "declare multiple enums at a time" do klass = Class.new(ActiveRecord::Base) do self.table_name = "books" @@ -537,4 +556,13 @@ class EnumTest < ActiveRecord::TestCase test "data type of Enum type" do assert_equal :integer, Book.type_for_attribute("status").type end + + test "scopes can be disabled" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written], _scopes: false + end + + assert_raises(NoMethodError) { klass.proposed } + end end diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb index b90e6a66c5..0d2be944b5 100644 --- a/activerecord/test/cases/errors_test.rb +++ b/activerecord/test/cases/errors_test.rb @@ -8,11 +8,9 @@ class ErrorsTest < ActiveRecord::TestCase error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base } (error_klasses - [ActiveRecord::AmbiguousSourceReflectionForThroughAssociation]).each do |error_klass| - begin - error_klass.new.inspect - rescue ArgumentError - raise "Instance of #{error_klass} can't be initialized with no arguments" - end + error_klass.new.inspect + rescue ArgumentError + raise "Instance of #{error_klass} can't be initialized with no arguments" end end end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index a0e75f4e89..edd2c768d3 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -72,7 +72,6 @@ if ActiveRecord::Base.connection.supports_explain? end private - def stub_explain_for_query_plans(query_plans = ["query plan foo", "query plan bar"]) explain_called = 0 diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb index 47161a547a..2f4c9b0ef7 100644 --- a/activerecord/test/cases/filter_attributes_test.rb +++ b/activerecord/test/cases/filter_attributes_test.rb @@ -90,15 +90,13 @@ class FilterAttributesTest < ActiveRecord::TestCase end test "filter_attributes should handle [FILTERED] value properly" do - begin - User.filter_attributes = ["auth"] - user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") - assert_includes user.inspect, "auth_token: [FILTERED]" - assert_includes user.inspect, 'token: "[FILTERED]"' - ensure - User.remove_instance_variable(:@filter_attributes) - end + assert_includes user.inspect, "auth_token: [FILTERED]" + assert_includes user.inspect, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) end test "filter_attributes on pretty_print" do @@ -121,16 +119,14 @@ class FilterAttributesTest < ActiveRecord::TestCase end test "filter_attributes on pretty_print should handle [FILTERED] value properly" do - begin - User.filter_attributes = ["auth"] - user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") - actual = "".dup - PP.pp(user, StringIO.new(actual)) + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + actual = "".dup + PP.pp(user, StringIO.new(actual)) - assert_includes actual, "auth_token: [FILTERED]" - assert_includes actual, 'token: "[FILTERED]"' - ensure - User.remove_instance_variable(:@filter_attributes) - end + assert_includes actual, "auth_token: [FILTERED]" + assert_includes actual, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) end end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index e0acd30c22..d9e88b3feb 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -12,10 +12,10 @@ class FinderRespondToTest < ActiveRecord::TestCase end def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method - class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { } + Topic.singleton_class.define_method(:method_added_for_finder_respond_to_test) { } assert_respond_to Topic, :method_added_for_finder_respond_to_test ensure - class << Topic; self; end.send(:remove_method, :method_added_for_finder_respond_to_test) + Topic.singleton_class.remove_method :method_added_for_finder_respond_to_test end def test_should_preserve_normal_respond_to_behaviour_and_respond_to_standard_object_method @@ -54,8 +54,7 @@ class FinderRespondToTest < ActiveRecord::TestCase end private - def ensure_topic_method_is_not_cached(method_id) - class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.include? method_id + Topic.singleton_class.remove_method method_id if Topic.public_methods.include? method_id end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 52fd9291b2..1f2058cc0a 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -3,6 +3,7 @@ require "cases/helper" require "models/post" require "models/author" +require "models/account" require "models/categorization" require "models/comment" require "models/company" @@ -21,6 +22,7 @@ require "models/dog" require "models/car" require "models/tyre" require "models/subscriber" +require "support/stubs/strong_parameters" class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars @@ -224,6 +226,18 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Subscriber.exists?(" ") end + def test_exists_with_strong_parameters + assert_equal false, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!) + + Subscriber.create!(nick: "foo") + + assert_equal true, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!) + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Subscriber.exists?(ProtectedParams.new(nick: "foo")) + end + end + def test_exists_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.exists?(Topic.new) @@ -231,7 +245,8 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_does_not_select_columns_without_alias - assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do + c = Topic.connection + assert_sql(/SELECT 1 AS one FROM #{Regexp.escape(c.quote_table_name("topics"))}/i) do Topic.exists? end end @@ -258,6 +273,21 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.exists?({}) end + def test_exists_with_distinct_and_offset_and_joins + assert Post.left_joins(:comments).distinct.offset(10).exists? + assert_not Post.left_joins(:comments).distinct.offset(11).exists? + end + + def test_exists_with_distinct_and_offset_and_select + assert Post.select(:body).distinct.offset(3).exists? + assert_not Post.select(:body).distinct.offset(4).exists? + end + + def test_exists_with_distinct_and_offset_and_eagerload_and_order + assert Post.eager_load(:comments).distinct.offset(10).merge(Comment.order(post_id: :asc)).exists? + assert_not Post.eager_load(:comments).distinct.offset(11).merge(Comment.order(post_id: :asc)).exists? + end + # Ensure +exists?+ runs without an error by excluding distinct value. # See https://github.com/rails/rails/pull/26981. def test_exists_with_order_and_distinct @@ -269,6 +299,17 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists? end + def test_exists_with_large_number + assert_equal true, Topic.where(id: [1, 9223372036854775808]).exists? + assert_equal true, Topic.where(id: 1..9223372036854775808).exists? + assert_equal true, Topic.where(id: -9223372036854775809..9223372036854775808).exists? + assert_equal false, Topic.where(id: 9223372036854775808..9223372036854775809).exists? + assert_equal false, Topic.where(id: -9223372036854775810..-9223372036854775809).exists? + assert_equal false, Topic.where(id: 9223372036854775808..1).exists? + assert_equal true, Topic.where(id: 1).or(Topic.where(id: 9223372036854775808)).exists? + assert_equal true, Topic.where.not(id: 9223372036854775808).exists? + end + def test_exists_with_joins assert_equal true, Topic.joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? end @@ -370,16 +411,19 @@ class FinderTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordNotFound) do Topic.where("1=1").find(9999999999999999999999999999999) end + assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find(1) end def test_find_by_on_relation_with_large_number assert_nil Topic.where("1=1").find_by(id: 9999999999999999999999999999999) + assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by(id: 1) end def test_find_by_bang_on_relation_with_large_number assert_raises(ActiveRecord::RecordNotFound) do Topic.where("1=1").find_by!(id: 9999999999999999999999999999999) end + assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by!(id: 1) end def test_find_an_empty_array @@ -424,14 +468,14 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_association_subquery - author = authors(:david) - assert_equal author.post, Post.find_by(author: Author.where(id: author)) - assert_equal author.post, Post.find_by(author_id: Author.where(id: author)) + firm = companies(:first_firm) + assert_equal firm.account, Account.find_by(firm: Firm.where(id: firm)) + assert_equal firm.account, Account.find_by(firm_id: Firm.where(id: firm)) end def test_find_by_and_where_consistency_with_active_record_instance - author = authors(:david) - assert_equal Post.where(author_id: author).take, Post.find_by(author_id: author) + firm = companies(:first_firm) + assert_equal Account.where(firm_id: firm).take, Account.find_by(firm_id: firm) end def test_take @@ -479,6 +523,7 @@ class FinderTest < ActiveRecord::TestCase expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.first assert_equal expected, Topic.limit(5).first + assert_equal expected, Topic.order(nil).first end def test_model_class_responds_to_first_bang @@ -502,6 +547,7 @@ class FinderTest < ActiveRecord::TestCase expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.second assert_equal expected, Topic.limit(5).second + assert_equal expected, Topic.order(nil).second end def test_model_class_responds_to_second_bang @@ -525,6 +571,7 @@ class FinderTest < ActiveRecord::TestCase expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.third assert_equal expected, Topic.limit(5).third + assert_equal expected, Topic.order(nil).third end def test_model_class_responds_to_third_bang @@ -548,6 +595,7 @@ class FinderTest < ActiveRecord::TestCase expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.fourth assert_equal expected, Topic.limit(5).fourth + assert_equal expected, Topic.order(nil).fourth end def test_model_class_responds_to_fourth_bang @@ -571,6 +619,7 @@ class FinderTest < ActiveRecord::TestCase expected.touch # PostgreSQL changes the default order if no order clause is used assert_equal expected, Topic.fifth assert_equal expected, Topic.limit(5).fifth + assert_equal expected, Topic.order(nil).fifth end def test_model_class_responds_to_fifth_bang @@ -739,6 +788,17 @@ class FinderTest < ActiveRecord::TestCase assert_equal expected, clients.first(2) assert_equal expected, clients.limit(5).first(2) + assert_equal expected, clients.order(nil).first(2) + end + + def test_implicit_order_column_is_configurable + old_implicit_order_column = Topic.implicit_order_column + Topic.implicit_order_column = "title" + + assert_equal topics(:fifth), Topic.first + assert_equal topics(:third), Topic.last + ensure + Topic.implicit_order_column = old_implicit_order_column end def test_take_and_first_and_last_with_integer_should_return_an_array @@ -928,6 +988,7 @@ class FinderTest < ActiveRecord::TestCase assert_kind_of Money, zaphod_balance found_customers = Customer.where(balance: [david_balance, zaphod_balance]) assert_equal [customers(:david), customers(:zaphod)], found_customers.sort_by(&:id) + assert_equal Customer.where(balance: [david_balance.amount, zaphod_balance.amount]).to_sql, found_customers.to_sql end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate @@ -965,6 +1026,24 @@ class FinderTest < ActiveRecord::TestCase assert_equal customers(:david), found_customer end + def test_hash_condition_find_nil_with_aggregate_having_one_mapping + assert_nil customers(:zaphod).gps_location + found_customer = Customer.where(gps_location: nil, name: customers(:zaphod).name).first + assert_equal customers(:zaphod), found_customer + end + + def test_hash_condition_find_nil_with_aggregate_having_multiple_mappings + customers(:david).update(address: nil) + assert_nil customers(:david).address_street + assert_nil customers(:david).address_city + found_customer = Customer.where(address: nil, name: customers(:david).name).first + assert_equal customers(:david), found_customer + end + + def test_hash_condition_find_empty_array_with_aggregate_having_multiple_mappings + assert_nil Customer.where(address: []).first + end + def test_condition_utc_time_interpolation_with_default_timezone_local with_env_tz "America/New_York" do with_timezone_config default: :local do @@ -1106,7 +1185,7 @@ class FinderTest < ActiveRecord::TestCase def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching # ensure this test can run independently of order - class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.include?(:find_by_credit_limit) + Account.singleton_class.remove_method :find_by_credit_limit if Account.public_methods.include?(:find_by_credit_limit) a = Account.where("firm_id = ?", 6).find_by_credit_limit(50) assert_equal a, Account.where("firm_id = ?", 6).find_by_credit_limit(50) # find_by_credit_limit has been cached end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index a5592fc86a..a7f01e898e 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -73,14 +73,12 @@ class FixturesTest < ActiveRecord::TestCase if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_bulk_insert - begin - subscriber = InsertQuerySubscriber.new - subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) - create_fixtures("bulbs") - assert_equal 1, subscriber.events.size, "It takes one INSERT query to insert two fixtures" - ensure - ActiveSupport::Notifications.unsubscribe(subscription) - end + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + create_fixtures("bulbs") + assert_equal 1, subscriber.events.size, "It takes one INSERT query to insert two fixtures" + ensure + ActiveSupport::Notifications.unsubscribe(subscription) end def test_bulk_insert_multiple_table_with_a_multi_statement_query @@ -211,7 +209,7 @@ class FixturesTest < ActiveRecord::TestCase conn = ActiveRecord::Base.connection mysql_margin = 2 packet_size = 1024 - bytes_needed_to_have_a_1024_bytes_fixture = 858 + bytes_needed_to_have_a_1024_bytes_fixture = 906 fixtures = { "traffic_lights" => [ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] }, @@ -303,20 +301,6 @@ class FixturesTest < ActiveRecord::TestCase assert_equal fixtures, result.to_a end - def test_deprecated_insert_fixtures - fixtures = [ - { "name" => "first", "wheels_count" => 2 }, - { "name" => "second", "wheels_count" => 3 } - ] - conn = ActiveRecord::Base.connection - conn.delete("DELETE FROM aircraft") - assert_deprecated do - conn.insert_fixtures(fixtures, "aircraft") - end - result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id") - assert_equal fixtures, result.to_a - end - def test_broken_yaml_exception badyaml = Tempfile.new ["foo", ".yml"] badyaml.write "a: : " @@ -512,11 +496,7 @@ class FixturesTest < ActiveRecord::TestCase ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") end - if current_adapter?(:SQLite3Adapter) - assert_equal(%(table "parrots" has no column named "arrr".), e.message) - else - assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message) - end + assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message) end def test_yaml_file_with_symbol_columns @@ -940,7 +920,7 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase def lock_thread=(lock_thread); end end.new - assert_called_with(connection, :begin_transaction, [joinable: false]) do + assert_called_with(connection, :begin_transaction, [joinable: false, _lazy: false]) do fire_connection_notification(connection) end end @@ -968,7 +948,6 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase end private - def fire_connection_notification(connection) assert_called_with(ActiveRecord::Base.connection_handler, :retrieve_connection, ["book"], returns: connection) do message_bus = ActiveSupport::Notifications.instrumenter @@ -1364,3 +1343,36 @@ class NilFixturePathTest < ActiveRecord::TestCase MSG end end + +class MultipleDatabaseFixturesTest < ActiveRecord::TestCase + test "enlist_fixture_connections ensures multiple databases share a connection pool" do + with_temporary_connection_pool do + ActiveRecord::Base.connects_to database: { writing: :arunit, reading: :arunit2 } + + rw_conn = ActiveRecord::Base.connection + ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection + + assert_not_equal rw_conn, ro_conn + + enlist_fixture_connections + + rw_conn = ActiveRecord::Base.connection + ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection + + assert_equal rw_conn, ro_conn + end + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.connection_handler } + end + + private + def with_temporary_connection_pool + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) + new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool + + yield + ensure + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool + end +end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index 101fa118c8..e7e31b6d2d 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -1,48 +1,12 @@ # frozen_string_literal: true require "cases/helper" -require "active_support/core_ext/hash/indifferent_access" - require "models/company" require "models/person" require "models/ship" require "models/ship_part" require "models/treasure" - -class ProtectedParams - attr_accessor :permitted - alias :permitted? :permitted - - delegate :keys, :key?, :has_key?, :empty?, to: :@parameters - - def initialize(attributes) - @parameters = attributes.with_indifferent_access - @permitted = false - end - - def permit! - @permitted = true - self - end - - def [](key) - @parameters[key] - end - - def to_h - @parameters - end - - def stringify_keys - dup - end - - def dup - super.tap do |duplicate| - duplicate.instance_variable_set :@permitted, @permitted - end - end -end +require "support/stubs/strong_parameters" class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase def test_forbidden_attributes_cannot_be_used_for_mass_assignment diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb index b15e1b48c4..9dbd339fe7 100644 --- a/activerecord/test/cases/habtm_destroy_order_test.rb +++ b/activerecord/test/cases/habtm_destroy_order_test.rb @@ -30,23 +30,21 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase test "not destroying a student with lessons leaves student<=>lesson association intact" do # test a normal before_destroy doesn't destroy the habtm joins - begin - sicp = Lesson.new(name: "SICP") - ben = Student.new(name: "Ben Bitdiddle") - # add a before destroy to student - Student.class_eval do - before_destroy do - raise ActiveRecord::Rollback unless lessons.empty? - end + sicp = Lesson.new(name: "SICP") + ben = Student.new(name: "Ben Bitdiddle") + # add a before destroy to student + Student.class_eval do + before_destroy do + raise ActiveRecord::Rollback unless lessons.empty? end - ben.lessons << sicp - ben.save! - ben.destroy - assert_not_empty ben.reload.lessons - ensure - # get rid of it so Student is still like it was - Student.reset_callbacks(:destroy) end + ben.lessons << sicp + ben.save! + ben.destroy + assert_not_empty ben.reload.lessons + ensure + # get rid of it so Student is still like it was + Student.reset_callbacks(:destroy) end test "not destroying a lesson with students leaves student<=>lesson association intact" do diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 730cd663a2..56c780c4a6 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -53,12 +53,22 @@ def supports_default_expression? true elsif current_adapter?(:Mysql2Adapter) conn = ActiveRecord::Base.connection - !conn.mariadb? && conn.version >= "8.0.13" + !conn.mariadb? && conn.database_version >= "8.0.13" end end -def supports_savepoints? - ActiveRecord::Base.connection.supports_savepoints? +%w[ + supports_savepoints? + supports_partial_index? + supports_insert_returning? + supports_insert_on_duplicate_skip? + supports_insert_on_duplicate_update? + supports_insert_conflict_target? + supports_optimizer_hints? +].each do |method_name| + define_method method_name do + ActiveRecord::Base.connection.public_send(method_name) + end end def with_env_tz(new_tz = "US/Eastern") @@ -179,7 +189,6 @@ end module InTimeZone private - def in_time_zone(zone) old_zone = Time.zone old_tz = ActiveRecord::Base.time_zone_aware_attributes @@ -192,3 +201,5 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end + +require_relative "../../../tools/test_common" diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index 7b388ebc5e..f41aea6125 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -115,7 +115,6 @@ class HotCompatibilityTest < ActiveRecord::TestCase end private - def get_prepared_statement_cache(connection) connection.instance_variable_get(:@statements) .instance_variable_get(:@cache)[Process.pid] diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 3d3189900f..01e4878c3f 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -240,7 +240,7 @@ class InheritanceTest < ActiveRecord::TestCase cabbage = vegetable.becomes!(Cabbage) assert_equal "Cabbage", cabbage.custom_type - vegetable = cabbage.becomes!(Vegetable) + cabbage.becomes!(Vegetable) assert_nil cabbage.custom_type end @@ -471,9 +471,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_eager_load_belongs_to_primary_key_quoting - con = Account.connection + c = Account.connection bind_param = Arel::Nodes::BindParam.new(nil) - assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = (?:#{Regexp.quote(bind_param.to_sql)}|1)/) do + assert_sql(/#{Regexp.escape(c.quote_table_name("companies.id"))} = (?:#{Regexp.escape(bind_param.to_sql)}|1)/i) do Account.all.merge!(includes: :firm).find(1) end end @@ -514,10 +514,12 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase # Should fail without FirmOnTheFly in the type condition. assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } + assert_raise(ActiveRecord::RecordNotFound) { Firm.find_by!(id: foo.id) } # Nest FirmOnTheFly in the test case where Dependencies won't see it. self.class.const_set :FirmOnTheFly, Class.new(Firm) assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } + assert_raise(ActiveRecord::SubclassNotFound) { Firm.find_by!(id: foo.id) } # Nest FirmOnTheFly in Firm where Dependencies will see it. # This is analogous to nesting models in a migration. @@ -526,6 +528,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase # And instantiate will find the existing constant rather than trying # to require firm_on_the_fly. assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } + assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find_by!(id: foo.id) } end end @@ -654,7 +657,7 @@ class InheritanceAttributeMappingTest < ActiveRecord::TestCase assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values("SELECT sponsorable_type FROM sponsors") - sponsor = Sponsor.first + sponsor = Sponsor.find(sponsor.id) assert_equal startup, sponsor.sponsorable end end diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb new file mode 100644 index 0000000000..d086d77081 --- /dev/null +++ b/activerecord/test/cases/insert_all_test.rb @@ -0,0 +1,275 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/book" + +class ReadonlyNameBook < Book + attr_readonly :name +end + +class InsertAllTest < ActiveRecord::TestCase + fixtures :books + + def test_insert + skip unless supports_insert_on_duplicate_skip? + + id = 1_000_000 + + assert_difference "Book.count", +1 do + Book.insert(id: id, name: "Rework", author_id: 1) + end + + Book.upsert(id: id, name: "Remote", author_id: 1) + + assert_equal "Remote", Book.find(id).name + end + + def test_insert! + assert_difference "Book.count", +1 do + Book.insert! name: "Rework", author_id: 1 + end + end + + def test_insert_all + assert_difference "Book.count", +10 do + Book.insert_all! [ + { name: "Rework", author_id: 1 }, + { name: "Patterns of Enterprise Application Architecture", author_id: 1 }, + { name: "Design of Everyday Things", author_id: 1 }, + { name: "Practical Object-Oriented Design in Ruby", author_id: 1 }, + { name: "Clean Code", author_id: 1 }, + { name: "Ruby Under a Microscope", author_id: 1 }, + { name: "The Principles of Product Development Flow", author_id: 1 }, + { name: "Peopleware", author_id: 1 }, + { name: "About Face", author_id: 1 }, + { name: "Eloquent Ruby", author_id: 1 }, + ] + end + end + + def test_insert_all_should_handle_empty_arrays + assert_raise ArgumentError do + Book.insert_all! [] + end + end + + def test_insert_all_raises_on_duplicate_records + assert_raise ActiveRecord::RecordNotUnique do + Book.insert_all! [ + { name: "Rework", author_id: 1 }, + { name: "Patterns of Enterprise Application Architecture", author_id: 1 }, + { name: "Agile Web Development with Rails", author_id: 1 }, + ] + end + end + + def test_insert_all_returns_ActiveRecord_Result + result = Book.insert_all! [{ name: "Rework", author_id: 1 }] + assert_kind_of ActiveRecord::Result, result + end + + def test_insert_all_returns_primary_key_if_returning_is_supported + skip unless supports_insert_returning? + + result = Book.insert_all! [{ name: "Rework", author_id: 1 }] + assert_equal %w[ id ], result.columns + end + + def test_insert_all_returns_nothing_if_returning_is_empty + skip unless supports_insert_returning? + + result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: [] + assert_equal [], result.columns + end + + def test_insert_all_returns_nothing_if_returning_is_false + skip unless supports_insert_returning? + + result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: false + assert_equal [], result.columns + end + + def test_insert_all_returns_requested_fields + skip unless supports_insert_returning? + + result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: [:id, :name] + assert_equal %w[ Rework ], result.pluck("name") + end + + def test_insert_all_can_skip_duplicate_records + skip unless supports_insert_on_duplicate_skip? + + assert_no_difference "Book.count" do + Book.insert_all [{ id: 1, name: "Agile Web Development with Rails" }] + end + end + + def test_insert_all_with_skip_duplicates_and_autonumber_id_not_given + skip unless supports_insert_on_duplicate_skip? + + assert_difference "Book.count", 1 do + # These two books are duplicates according to an index on %i[author_id name] + # but their IDs are not specified so they will be assigned different IDs + # by autonumber. We will get an exception from MySQL if we attempt to skip + # one of these records by assigning its ID. + Book.insert_all [ + { author_id: 8, name: "Refactoring" }, + { author_id: 8, name: "Refactoring" } + ] + end + end + + def test_insert_all_with_skip_duplicates_and_autonumber_id_given + skip unless supports_insert_on_duplicate_skip? + + assert_difference "Book.count", 1 do + Book.insert_all [ + { id: 200, author_id: 8, name: "Refactoring" }, + { id: 201, author_id: 8, name: "Refactoring" } + ] + end + end + + def test_skip_duplicates_strategy_does_not_secretly_upsert + skip unless supports_insert_on_duplicate_skip? + + book = Book.create!(author_id: 8, name: "Refactoring", format: "EXPECTED") + + assert_no_difference "Book.count" do + Book.insert(author_id: 8, name: "Refactoring", format: "UNEXPECTED") + end + + assert_equal "EXPECTED", book.reload.format + end + + def test_insert_all_will_raise_if_duplicates_are_skipped_only_for_a_certain_conflict_target + skip unless supports_insert_on_duplicate_skip? && supports_insert_conflict_target? + + assert_raise ActiveRecord::RecordNotUnique do + Book.insert_all [{ id: 1, name: "Agile Web Development with Rails" }], + unique_by: :index_books_on_author_id_and_name + end + end + + def test_insert_all_and_upsert_all_with_index_finding_options + skip unless supports_insert_conflict_target? + + assert_difference "Book.count", +3 do + Book.insert_all [{ name: "Rework", author_id: 1 }], unique_by: :isbn + Book.insert_all [{ name: "Remote", author_id: 1 }], unique_by: %i( author_id name ) + Book.insert_all [{ name: "Renote", author_id: 1 }], unique_by: :index_books_on_isbn + end + + assert_raise ActiveRecord::RecordNotUnique do + Book.upsert_all [{ name: "Rework", author_id: 1 }], unique_by: :isbn + end + end + + def test_insert_all_and_upsert_all_raises_when_index_is_missing + skip unless supports_insert_conflict_target? + + [ :cats, %i( author_id isbn ), :author_id ].each do |missing_or_non_unique_by| + error = assert_raises ArgumentError do + Book.insert_all [{ name: "Rework", author_id: 1 }], unique_by: missing_or_non_unique_by + end + assert_match "No unique index", error.message + + error = assert_raises ArgumentError do + Book.upsert_all [{ name: "Rework", author_id: 1 }], unique_by: missing_or_non_unique_by + end + assert_match "No unique index", error.message + end + end + + def test_insert_logs_message_including_model_name + skip unless supports_insert_conflict_target? + + capture_log_output do |output| + Book.insert(name: "Rework", author_id: 1) + assert_match "Book Insert", output.string + end + end + + def test_insert_all_logs_message_including_model_name + skip unless supports_insert_conflict_target? + + capture_log_output do |output| + Book.insert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }] + assert_match "Book Bulk Insert", output.string + end + end + + def test_upsert_logs_message_including_model_name + skip unless supports_insert_on_duplicate_update? + + capture_log_output do |output| + Book.upsert(name: "Remote", author_id: 1) + assert_match "Book Upsert", output.string + end + end + + def test_upsert_all_logs_message_including_model_name + skip unless supports_insert_on_duplicate_update? + + capture_log_output do |output| + Book.upsert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }] + assert_match "Book Bulk Upsert", output.string + end + end + + def test_upsert_all_updates_existing_records + skip unless supports_insert_on_duplicate_update? + + new_name = "Agile Web Development with Rails, 4th Edition" + Book.upsert_all [{ id: 1, name: new_name }] + assert_equal new_name, Book.find(1).name + end + + def test_upsert_all_does_not_update_readonly_attributes + skip unless supports_insert_on_duplicate_update? + + new_name = "Agile Web Development with Rails, 4th Edition" + ReadonlyNameBook.upsert_all [{ id: 1, name: new_name }] + assert_not_equal new_name, Book.find(1).name + end + + def test_upsert_all_does_not_update_primary_keys + skip unless supports_insert_on_duplicate_update? && supports_insert_conflict_target? + + Book.upsert_all [{ id: 101, name: "Perelandra", author_id: 7 }] + Book.upsert_all [{ id: 103, name: "Perelandra", author_id: 7, isbn: "1974522598" }], + unique_by: :index_books_on_author_id_and_name + + book = Book.find_by(name: "Perelandra") + assert_equal 101, book.id, "Should not have updated the ID" + assert_equal "1974522598", book.isbn, "Should have updated the isbn" + end + + def test_upsert_all_does_not_perform_an_upsert_if_a_partial_index_doesnt_apply + skip unless supports_insert_on_duplicate_update? && supports_insert_conflict_target? && supports_partial_index? + + Book.upsert_all [{ name: "Out of the Silent Planet", author_id: 7, isbn: "1974522598", published_on: Date.new(1938, 4, 1) }] + Book.upsert_all [{ name: "Perelandra", author_id: 7, isbn: "1974522598" }], + unique_by: :index_books_on_isbn + + assert_equal ["Out of the Silent Planet", "Perelandra"], Book.where(isbn: "1974522598").order(:name).pluck(:name) + end + + def test_insert_all_raises_on_unknown_attribute + assert_raise ActiveRecord::UnknownAttributeError do + Book.insert_all! [{ unknown_attribute: "Test" }] + end + end + + private + def capture_log_output + output = StringIO.new + old_logger, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ActiveSupport::Logger.new(output) + + begin + yield output + ensure + ActiveRecord::Base.logger = old_logger + end + end +end diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb index e6e8468757..06382b6c7c 100644 --- a/activerecord/test/cases/instrumentation_test.rb +++ b/activerecord/test/cases/instrumentation_test.rb @@ -5,6 +5,10 @@ require "models/book" module ActiveRecord class InstrumentationTest < ActiveRecord::TestCase + def setup + ActiveRecord::Base.connection.schema_cache.add(Book.table_name) + end + def test_payload_name_on_load Book.create(name: "test book") subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| @@ -37,8 +41,8 @@ module ActiveRecord assert_equal "Book Update", event.payload[:name] end end - book = Book.create(name: "test book") - book.update_attribute(:name, "new name") + book = Book.create(name: "test book", format: "paperback") + book.update_attribute(:format, "ebook") ensure ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end @@ -50,8 +54,8 @@ module ActiveRecord assert_equal "Book Update All", event.payload[:name] end end - Book.create(name: "test book") - Book.update_all(name: "new name") + Book.create(name: "test book", format: "paperback") + Book.update_all(format: "ebook") ensure ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 5687afbc71..4185e8d682 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -191,21 +191,6 @@ class IntegrationTest < ActiveRecord::TestCase end end - def test_named_timestamps_for_cache_key - assert_deprecated do - owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) - end - end - - def test_cache_key_when_named_timestamp_is_nil - assert_deprecated do - owner = owners(:blackbeard) - owner.happy_at = nil - assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) - end - end - def test_cache_key_is_stable_with_versioning_on with_cache_versioning do developer = Developer.first diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 6cf17ac15d..7f67b945f0 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -103,6 +103,32 @@ module ActiveRecord end end + class ChangeColumnComment1 < SilentMigration + def change + create_table("horses") do |t| + t.column :name, :string, comment: "Sekitoba" + end + end + end + + class ChangeColumnComment2 < SilentMigration + def change + change_column_comment :horses, :name, from: "Sekitoba", to: "Diomed" + end + end + + class ChangeTableComment1 < SilentMigration + def change + create_table("horses", comment: "Sekitoba") + end + end + + class ChangeTableComment2 < SilentMigration + def change + change_table_comment :horses, from: "Sekitoba", to: "Diomed" + end + end + class DisableExtension1 < SilentMigration def change enable_extension "hstore" @@ -290,6 +316,7 @@ module ActiveRecord def test_migrate_revert_change_column_default migration1 = ChangeColumnDefault1.new migration1.migrate(:up) + Horse.reset_column_information assert_equal "Sekitoba", Horse.new.name migration2 = ChangeColumnDefault2.new @@ -302,12 +329,46 @@ module ActiveRecord assert_equal "Sekitoba", Horse.new.name end + if ActiveRecord::Base.connection.supports_comments? + def test_migrate_revert_change_column_comment + migration1 = ChangeColumnComment1.new + migration1.migrate(:up) + Horse.reset_column_information + assert_equal "Sekitoba", Horse.columns_hash["name"].comment + + migration2 = ChangeColumnComment2.new + migration2.migrate(:up) + Horse.reset_column_information + assert_equal "Diomed", Horse.columns_hash["name"].comment + + migration2.migrate(:down) + Horse.reset_column_information + assert_equal "Sekitoba", Horse.columns_hash["name"].comment + end + + def test_migrate_revert_change_table_comment + connection = ActiveRecord::Base.connection + migration1 = ChangeTableComment1.new + migration1.migrate(:up) + assert_equal "Sekitoba", connection.table_comment("horses") + + migration2 = ChangeTableComment2.new + migration2.migrate(:up) + assert_equal "Diomed", connection.table_comment("horses") + + migration2.migrate(:down) + assert_equal "Sekitoba", connection.table_comment("horses") + end + end + if current_adapter?(:PostgreSQLAdapter) def test_migrate_enable_and_disable_extension migration1 = InvertibleMigration.new migration2 = DisableExtension1.new migration3 = DisableExtension2.new + assert_equal true, Horse.connection.extension_available?("hstore") + migration1.migrate(:up) migration2.migrate(:up) assert_equal true, Horse.connection.extension_enabled?("hstore") diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 82cf281cff..d68e208617 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -10,7 +10,6 @@ require "models/comment" module JsonSerializationHelpers private - def set_include_root_in_json(value) original_root_in_json = ActiveRecord::Base.include_root_in_json ActiveRecord::Base.include_root_in_json = value @@ -24,7 +23,7 @@ class JsonSerializationTest < ActiveRecord::TestCase include JsonSerializationHelpers class NamespacedContact < Contact - column :name, :string + column :name, "string" end def setup diff --git a/activerecord/test/cases/legacy_configurations_test.rb b/activerecord/test/cases/legacy_configurations_test.rb deleted file mode 100644 index c36feb5116..0000000000 --- a/activerecord/test/cases/legacy_configurations_test.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require "cases/helper" - -module ActiveRecord - class LegacyConfigurationsTest < ActiveRecord::TestCase - def test_can_turn_configurations_into_a_hash - assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not." - assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort - end - - def test_each_is_deprecated - assert_deprecated do - ActiveRecord::Base.configurations.each do |db_config| - assert_equal "primary", db_config.spec_name - end - end - end - - def test_first_is_deprecated - assert_deprecated do - db_config = ActiveRecord::Base.configurations.first - assert_equal "arunit", db_config.env_name - assert_equal "primary", db_config.spec_name - end - end - - def test_fetch_is_deprecated - assert_deprecated do - db_config = ActiveRecord::Base.configurations.fetch("arunit").first - assert_equal "arunit", db_config.env_name - assert_equal "primary", db_config.spec_name - end - end - - def test_values_are_deprecated - config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config) - assert_deprecated do - assert_equal config_hashes, ActiveRecord::Base.configurations.values - end - end - end -end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 33bd74e114..b468da7c76 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -182,7 +182,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1.touch assert_equal 1, p1.lock_version - assert_not p1.changed?, "Changes should have been cleared" + assert_not_predicate p1, :changed?, "Changes should have been cleared" + assert_predicate p1, :saved_changes? + assert_equal ["lock_version", "updated_at"], p1.saved_changes.keys.sort end def test_touch_stale_object @@ -193,6 +195,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raises(ActiveRecord::StaleObjectError) do stale_person.touch end + + assert_not_predicate stale_person, :saved_changes? end def test_update_with_dirty_primary_key @@ -296,6 +300,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase t1.touch assert_equal 1, t1.lock_version + assert_not_predicate t1, :changed? + assert_predicate t1, :saved_changes? + assert_equal ["lock_version", "updated_at"], t1.saved_changes.keys.sort end def test_touch_stale_object_with_lock_without_default @@ -307,6 +314,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raises(ActiveRecord::StaleObjectError) do stale_object.touch end + + assert_not_predicate stale_object, :saved_changes? end def test_lock_without_default_should_work_with_null_in_the_database @@ -584,7 +593,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase end private - def add_counter_column_to(model, col = "test_count") model.connection.add_column model.table_name, col, :integer, null: false, default: 0 model.reset_column_information diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 7777508349..cc0587fa50 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -462,7 +462,11 @@ module ActiveRecord 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) + if current_adapter?(:Mysql2Adapter) + skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" + elsif current_adapter?(:SQLite3Adapter) + skip "SQLite3 does not support DROP TABLE CASCADE syntax" + end # can't re-create table referenced by foreign key assert_raises(ActiveRecord::StatementInvalid) do @connection.create_table :trains, force: true diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 3022121f4c..b6064500ee 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -176,11 +176,9 @@ module ActiveRecord 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 + assert_raise(ArgumentError) { add_column :test_models, :integer_too_big, :integer, limit: 10 } + assert_raise(ArgumentError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff } + assert_raise(ArgumentError) { add_column :test_models, :binary_too_big, :binary, limit: 0xfffffffff } end end end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index cedd9c44e3..cce3461e18 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -109,18 +109,10 @@ module ActiveRecord 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 + assert_equal ["index_test_models_on_hat_style_and_size"], connection.indexes("test_models").map(&:name) 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 + assert_equal ["index_test_models_on_style_and_size"], connection.indexes("test_models").map(&:name) end def test_rename_column_does_not_rename_custom_named_index @@ -144,7 +136,7 @@ module ActiveRecord 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" + skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.database_version >= "10.2.8" add_column "test_models", :hat_size, :integer add_column "test_models", :hat_style, :string, limit: 100 @@ -318,6 +310,17 @@ module ActiveRecord ensure connection.drop_table(:my_table) rescue nil end + + def test_add_column_without_column_name + e = assert_raise ArgumentError do + connection.create_table "my_table", force: true do |t| + t.timestamp + end + end + assert_equal "Missing column name(s) for timestamp", e.message + ensure + connection.drop_table :my_table, if_exists: true + end end end end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 01f8628fc5..c9f3756b1f 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -182,6 +182,40 @@ module ActiveRecord assert_equal [:change_column_default, [:table, :column, from: false, to: true]], change end + if ActiveRecord::Base.connection.supports_comments? + def test_invert_change_column_comment + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column_comment, [:table, :column, "comment"] + end + end + + def test_invert_change_column_comment_with_from_and_to + change = @recorder.inverse_of :change_column_comment, [:table, :column, from: "old_value", to: "new_value"] + assert_equal [:change_column_comment, [:table, :column, from: "new_value", to: "old_value"]], change + end + + def test_invert_change_column_comment_with_from_and_to_with_nil + change = @recorder.inverse_of :change_column_comment, [:table, :column, from: nil, to: "new_value"] + assert_equal [:change_column_comment, [:table, :column, from: "new_value", to: nil]], change + end + + def test_invert_change_table_comment + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column_comment, [:table, :column, "comment"] + end + end + + def test_invert_change_table_comment_with_from_and_to + change = @recorder.inverse_of :change_table_comment, [:table, from: "old_value", to: "new_value"] + assert_equal [:change_table_comment, [:table, from: "new_value", to: "old_value"]], change + end + + def test_invert_change_table_comment_with_from_and_to_with_nil + change = @recorder.inverse_of :change_table_comment, [:table, from: nil, to: "new_value"] + assert_equal [:change_table_comment, [:table, from: "new_value", to: nil]], change + end + 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 diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 017ee7951e..ff2a694e66 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -12,6 +12,7 @@ module ActiveRecord def setup super @connection = ActiveRecord::Base.connection + @schema_migration = @connection.schema_migration @verbose_was = ActiveRecord::Migration.verbose ActiveRecord::Migration.verbose = false @@ -38,7 +39,7 @@ module ActiveRecord }.new assert connection.index_exists?(:testings, :foo, name: "custom_index_name") - assert_raise(StandardError) { ActiveRecord::Migrator.new(:up, [migration]).migrate } + assert_raise(StandardError) { ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate } assert connection.index_exists?(:testings, :foo, name: "custom_index_name") end @@ -53,7 +54,7 @@ module ActiveRecord }.new assert connection.index_exists?(:testings, :bar) - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate assert_not connection.index_exists?(:testings, :bar) end @@ -67,7 +68,7 @@ module ActiveRecord end }.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate assert_not connection.index_exists?(:more_testings, :foo_id) assert_not connection.index_exists?(:more_testings, :bar_id) @@ -84,10 +85,10 @@ module ActiveRecord end }.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], @schema_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 + assert connection.column_exists?(:more_testings, :created_at, null: true) + assert connection.column_exists?(:more_testings, :updated_at, null: true) ensure connection.drop_table :more_testings rescue nil end @@ -101,10 +102,27 @@ module ActiveRecord end }.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate - assert connection.columns(:testings).find { |c| c.name == "created_at" }.null - assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + assert connection.column_exists?(:testings, :created_at, null: true) + assert connection.column_exists?(:testings, :updated_at, null: true) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table_with_bulk + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + change_table :testings, bulk: true do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate + + assert connection.column_exists?(:testings, :created_at, null: true) + assert connection.column_exists?(:testings, :updated_at, null: true) + end end def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table @@ -114,10 +132,72 @@ module ActiveRecord end }.new - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate + + assert connection.column_exists?(:testings, :created_at, null: true) + assert connection.column_exists?(:testings, :updated_at, null: true) + end + + def test_timestamps_doesnt_set_precision_on_create_table + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + create_table :more_testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate + + assert connection.column_exists?(:more_testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:more_testings, :updated_at, null: false, **precision_implicit_default) + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_doesnt_set_precision_on_change_table + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + change_table :testings do |t| + t.timestamps default: Time.now + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate + + assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_doesnt_set_precision_on_change_table_with_bulk + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + change_table :testings, bulk: true do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate + + assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default) + end + end + + def test_timestamps_doesnt_set_precision_on_add_timestamps + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + add_timestamps :testings, default: Time.now + end + }.new + + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate - assert connection.columns(:testings).find { |c| c.name == "created_at" }.null - assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default) end def test_legacy_migrations_raises_exception_when_inherited @@ -141,6 +221,35 @@ module ActiveRecord end end + if ActiveRecord::Base.connection.supports_comments? + def test_change_column_comment_can_be_reverted + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + revert do + change_column_comment(:testings, :foo, "comment") + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate + assert connection.column_exists?(:testings, :foo, comment: "comment") + end + + def test_change_table_comment_can_be_reverted + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + revert do + change_table_comment(:testings, "comment") + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate + + assert_equal "comment", connection.table_comment("testings") + end + end + if current_adapter?(:PostgreSQLAdapter) class Testing < ActiveRecord::Base end @@ -153,12 +262,21 @@ module ActiveRecord }.new Testing.create! - ActiveRecord::Migrator.new(:up, [migration]).migrate + ActiveRecord::Migrator.new(:up, [migration], @schema_migration).migrate assert_equal ["foobar"], Testing.all.map(&:foo) ensure ActiveRecord::Base.clear_cache! end end + + private + def precision_implicit_default + if current_adapter?(:Mysql2Adapter) + { precision: 0 } + else + { precision: nil } + end + end 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 index e0cbb29dcf..0257545330 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -151,7 +151,6 @@ module ActiveRecord end private - def with_table_cleanup tables_before = connection.data_sources diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index bb233fbf74..5f1057f093 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -3,7 +3,7 @@ require "cases/helper" require "support/schema_dumping_helper" -if ActiveRecord::Base.connection.supports_foreign_keys_in_create? +if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ForeignKeyInCreateTest < ActiveRecord::TestCase @@ -31,24 +31,39 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create? belongs_to :rocket end - setup do - @connection = ActiveRecord::Base.connection - @connection.create_table "rockets", force: true do |t| - t.string :name + class CreateRocketsMigration < ActiveRecord::Migration::Current + def up + create_table :rockets do |t| + t.string :name + end + + create_table :astronauts do |t| + t.string :name + t.references :rocket, foreign_key: true + end end - @connection.create_table "astronauts", force: true do |t| - t.string :name - t.references :rocket, foreign_key: true + def down + drop_table :astronauts, if_exists: true + drop_table :rockets, if_exists: true end + end + + def setup + @connection = ActiveRecord::Base.connection + @migration = CreateRocketsMigration.new + silence_stream($stdout) { @migration.migrate(:up) } + Rocket.reset_table_name Rocket.reset_column_information + Astronaut.reset_table_name Astronaut.reset_column_information end - teardown do - @connection.drop_table "astronauts", if_exists: true - @connection.drop_table "rockets", if_exists: true + def teardown + silence_stream($stdout) { @migration.migrate(:down) } + Rocket.reset_table_name Rocket.reset_column_information + Astronaut.reset_table_name Astronaut.reset_column_information end @@ -56,53 +71,100 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create? rocket = Rocket.create!(name: "myrocket") rocket.astronauts << Astronaut.create! - @connection.change_column_null :rockets, :name, false + @connection.change_column_null Rocket.table_name, :name, false - foreign_keys = @connection.foreign_keys("astronauts") + foreign_keys = @connection.foreign_keys(Astronaut.table_name) assert_equal 1, foreign_keys.size fk = foreign_keys.first assert_equal "myrocket", Rocket.first.name - assert_equal "astronauts", fk.from_table - assert_equal "rockets", fk.to_table + assert_equal Astronaut.table_name, fk.from_table + assert_equal Rocket.table_name, fk.to_table end def test_rename_column_of_child_table rocket = Rocket.create!(name: "myrocket") rocket.astronauts << Astronaut.create! - @connection.rename_column :astronauts, :name, :astronaut_name + @connection.rename_column Astronaut.table_name, :name, :astronaut_name - foreign_keys = @connection.foreign_keys("astronauts") + foreign_keys = @connection.foreign_keys(Astronaut.table_name) assert_equal 1, foreign_keys.size fk = foreign_keys.first assert_equal "myrocket", Rocket.first.name - assert_equal "astronauts", fk.from_table - assert_equal "rockets", fk.to_table + assert_equal Astronaut.table_name, fk.from_table + assert_equal Rocket.table_name, fk.to_table end def test_rename_reference_column_of_child_table rocket = Rocket.create!(name: "myrocket") rocket.astronauts << Astronaut.create! - @connection.rename_column :astronauts, :rocket_id, :new_rocket_id + @connection.rename_column Astronaut.table_name, :rocket_id, :new_rocket_id - foreign_keys = @connection.foreign_keys("astronauts") + foreign_keys = @connection.foreign_keys(Astronaut.table_name) assert_equal 1, foreign_keys.size fk = foreign_keys.first assert_equal "myrocket", Rocket.first.name - assert_equal "astronauts", fk.from_table - assert_equal "rockets", fk.to_table + assert_equal Astronaut.table_name, fk.from_table + assert_equal Rocket.table_name, fk.to_table assert_equal "new_rocket_id", fk.options[:column] end + + def test_remove_reference_column_of_child_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.remove_column Astronaut.table_name, :rocket_id + + assert_empty @connection.foreign_keys(Astronaut.table_name) + end + + def test_remove_foreign_key_by_column + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.remove_foreign_key Astronaut.table_name, column: :rocket_id + + assert_empty @connection.foreign_keys(Astronaut.table_name) + end + + def test_remove_foreign_key_by_column_in_change_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.change_table Astronaut.table_name do |t| + t.remove_foreign_key column: :rocket_id + end + + assert_empty @connection.foreign_keys(Astronaut.table_name) + end + end + + class ForeignKeyChangeColumnWithPrefixTest < ForeignKeyChangeColumnTest + setup do + ActiveRecord::Base.table_name_prefix = "p_" + end + + teardown do + ActiveRecord::Base.table_name_prefix = nil + end + end + + class ForeignKeyChangeColumnWithSuffixTest < ForeignKeyChangeColumnTest + setup do + ActiveRecord::Base.table_name_suffix = "_s" + end + + teardown do + ActiveRecord::Base.table_name_suffix = nil + end end end end -end -if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ForeignKeyTest < ActiveRecord::TestCase @@ -141,7 +203,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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 + assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter) end def test_add_foreign_key_inferes_column @@ -155,7 +217,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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) + assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter) end def test_add_foreign_key_with_column @@ -169,7 +231,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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) + assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter) end def test_add_foreign_key_with_non_standard_primary_key @@ -188,7 +250,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal "space_shuttles", fk.to_table assert_equal "pk", fk.primary_key ensure - @connection.remove_foreign_key :astronauts, name: "custom_pk" + @connection.remove_foreign_key :astronauts, name: "custom_pk", to_table: "space_shuttles" @connection.drop_table :space_shuttles end @@ -262,6 +324,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end def test_foreign_key_exists_by_name + skip if current_adapter?(:SQLite3Adapter) + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk") @@ -293,6 +357,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end def test_remove_foreign_key_by_name + skip if current_adapter?(:SQLite3Adapter) + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" assert_equal 1, @connection.foreign_keys("astronauts").size @@ -301,9 +367,22 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end def test_remove_foreign_non_existing_foreign_key_raises - assert_raises ArgumentError do + e = assert_raises ArgumentError do @connection.remove_foreign_key :astronauts, :rockets end + assert_equal "Table 'astronauts' has no foreign key for rockets", e.message + end + + def test_remove_foreign_key_by_the_select_one_on_the_same_table + @connection.add_foreign_key :astronauts, :rockets + @connection.add_reference :astronauts, :myrocket, foreign_key: { to_table: :rockets } + + assert_equal 2, @connection.foreign_keys("astronauts").size + + @connection.remove_foreign_key :astronauts, :rockets, column: "myrocket_id" + + assert_equal [["astronauts", "rockets", "rocket_id"]], + @connection.foreign_keys("astronauts").map { |fk| [fk.from_table, fk.to_table, fk.column] } end if ActiveRecord::Base.connection.supports_validate_constraints? @@ -382,7 +461,11 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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 + if current_adapter?(:SQLite3Adapter) + assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id"$}, output + else + 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 end def test_schema_dumping_with_custom_fk_ignore_pattern @@ -436,7 +519,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current - def change + def up create_table(:schools) create_table(:classes) do |t| @@ -444,6 +527,11 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end add_foreign_key :classes, :schools end + + def down + drop_table :classes, if_exists: true + drop_table :schools, if_exists: true + end end def test_add_foreign_key_with_prefix @@ -468,30 +556,4 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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 index c056199140..da8bdc472a 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -34,7 +34,6 @@ module ActiveRecord end private - delegate(*CONNECTION_METHODS, to: :connection) end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index f8fecc83cd..5e688efc2b 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -158,14 +158,11 @@ module ActiveRecord 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", 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") + connection.add_index("testings", ["last_name", "first_name"]) connection.remove_index("testings", ["last_name", "first_name"]) diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 28f4cc124b..431047f957 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -17,19 +17,20 @@ module ActiveRecord def setup super - ActiveRecord::SchemaMigration.create_table - ActiveRecord::SchemaMigration.delete_all + @schema_migration = ActiveRecord::Base.connection.schema_migration + @schema_migration.create_table + @schema_migration.delete_all end teardown do - ActiveRecord::SchemaMigration.drop_table + @schema_migration.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 + ActiveRecord::Migrator.new(:up, migrations, @schema_migration).migrate ensure ActiveRecord::Base.logger = previous_logger end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 7a092103c7..90a50a5651 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -2,7 +2,7 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_foreign_keys_in_create? +if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase @@ -65,9 +65,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create? end end end -end -if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ReferencesForeignKeyTest < ActiveRecord::TestCase @@ -152,35 +150,38 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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 + 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 + 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 + 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 class CreateDogsMigration < ActiveRecord::Migration::Current - def change + def up create_table :dog_owners create_table :dogs do |t| t.references :dog_owner, foreign_key: true end end + + def down + drop_table :dogs, if_exists: true + drop_table :dog_owners, if_exists: true + end end def test_references_foreign_key_with_prefix @@ -234,24 +235,4 @@ if ActiveRecord::Base.connection.supports_foreign_keys? 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_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 769241ba12..451894fc54 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -126,7 +126,6 @@ module ActiveRecord end private - def with_polymorphic_column add_column table_name, :supplier_type, :string add_index table_name, [:supplier_id, :supplier_type] diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 661163b4a1..20f577b2c5 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -38,6 +38,7 @@ class MigrationTest < ActiveRecord::TestCase end Reminder.reset_column_information @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false + @schema_migration = ActiveRecord::Base.connection.schema_migration ActiveRecord::Base.connection.schema_cache.clear! end @@ -71,13 +72,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migration.verbose = @verbose_was end - def test_migrator_migrations_path_is_deprecated - assert_deprecated do - ActiveRecord::Migrator.migrations_path = "/whatever" - end - ensure + def test_passing_migrations_paths_to_assume_migrated_upto_version_is_deprecated + ActiveRecord::SchemaMigration.create_table assert_deprecated do - ActiveRecord::Migrator.migrations_path = "db/migrate" + ActiveRecord::Base.connection.assume_migrated_upto_version(0, []) end end @@ -87,7 +85,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" - migrator = ActiveRecord::MigrationContext.new(migrations_path) + migrator = ActiveRecord::MigrationContext.new(migrations_path, @schema_migration) migrator.up assert_equal 3, migrator.current_version @@ -105,23 +103,23 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true migrations_path = MIGRATIONS_ROOT + "/valid" - migrator = ActiveRecord::MigrationContext.new(migrations_path) + migrator = ActiveRecord::MigrationContext.new(migrations_path, @schema_migration) assert_equal true, migrator.needs_migration? end def test_any_migrations - migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid", @schema_migration) assert_predicate migrator, :any_migrations? - migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty") + migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty", @schema_migration) assert_not_predicate migrator_empty, :any_migrations? end def test_migration_version - migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check") + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check", @schema_migration) assert_equal 0, migrator.current_version migrator.up(20131219224947) assert_equal 20131219224947, migrator.current_version @@ -193,6 +191,7 @@ class MigrationTest < ActiveRecord::TestCase assert_not_predicate BigNumber, :table_exists? GiveMeBigNumbers.up + assert_predicate BigNumber, :table_exists? BigNumber.reset_column_information assert BigNumber.create( @@ -251,7 +250,7 @@ class MigrationTest < ActiveRecord::TestCase assert_not_predicate Reminder, :table_exists? name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" } - migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") + migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid", @schema_migration) migrator.up(&name_filter) assert_column Person, :last_name @@ -313,7 +312,7 @@ class MigrationTest < ActiveRecord::TestCase end }.new - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 100) e = assert_raise(StandardError) { migrator.migrate } @@ -334,7 +333,7 @@ class MigrationTest < ActiveRecord::TestCase end }.new - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 100) e = assert_raise(StandardError) { migrator.run } @@ -357,7 +356,7 @@ class MigrationTest < ActiveRecord::TestCase end }.new - migrator = ActiveRecord::Migrator.new(:up, [migration], 101) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 101) e = assert_raise(StandardError) { migrator.migrate } assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message @@ -388,6 +387,7 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "changed", ActiveRecord::SchemaMigration.table_name ensure ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name + ActiveRecord::SchemaMigration.reset_table_name Reminder.reset_table_name end @@ -408,13 +408,14 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "changed", ActiveRecord::InternalMetadata.table_name ensure ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + ActiveRecord::InternalMetadata.reset_table_name Reminder.reset_table_name end def test_internal_metadata_stores_environment current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" - migrator = ActiveRecord::MigrationContext.new(migrations_path) + migrator = ActiveRecord::MigrationContext.new(migrations_path, @schema_migration) migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] @@ -442,8 +443,7 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" - current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - migrator = ActiveRecord::MigrationContext.new(migrations_path) + migrator = ActiveRecord::MigrationContext.new(migrations_path, @schema_migration) migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] @@ -484,6 +484,7 @@ class MigrationTest < ActiveRecord::TestCase Thing.reset_table_name Thing.reset_sequence_name WeNeedThings.up + assert_predicate Thing, :table_exists? Thing.reset_column_information assert Thing.create("content" => "hello world") @@ -504,8 +505,9 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name Reminder.reset_sequence_name - Reminder.reset_column_information WeNeedReminders.up + assert_predicate Reminder, :table_exists? + Reminder.reset_column_information assert Reminder.create("content" => "hello world", "remind_at" => Time.now) assert_equal "hello world", Reminder.first.content @@ -571,75 +573,74 @@ class MigrationTest < ActiveRecord::TestCase end end - if current_adapter? :OracleAdapter - def test_create_table_with_custom_sequence_name - # table name is 29 chars, the standard sequence name will - # be 33 chars and should be shortened - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok do |t| - t.column :foo, :string, null: false - end - ensure - Person.connection.drop_table :table_with_name_thats_just_ok rescue nil - end - end - - # should be all good w/ a custom sequence name - assert_nothing_raised do - begin - Person.connection.create_table :table_with_name_thats_just_ok, - sequence_name: "suitably_short_seq" do |t| - t.column :foo, :string, null: false - end - - Person.connection.execute("select suitably_short_seq.nextval from dual") - - ensure - Person.connection.drop_table :table_with_name_thats_just_ok, - sequence_name: "suitably_short_seq" rescue nil - end - end - - # confirm the custom sequence got dropped - assert_raise(ActiveRecord::StatementInvalid) do - Person.connection.execute("select suitably_short_seq.nextval from dual") + def test_decimal_scale_without_precision_should_raise + e = assert_raise(ArgumentError) do + Person.connection.create_table :test_decimal_scales, force: true do |t| + t.decimal :scaleonly, scale: 10 end end + + assert_equal "Error adding decimal column: precision cannot be empty if scale is specified", e.message + ensure + Person.connection.drop_table :test_decimal_scales, if_exists: true end if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_integer_limit_should_raise - e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do + e = assert_raise(ArgumentError) do Person.connection.create_table :test_integer_limits, force: true do |t| t.column :bigone, :integer, limit: 10 end end - assert_match(/No integer type has byte size 10/, e.message) + assert_includes e.message, "No integer type has byte size 10" ensure Person.connection.drop_table :test_integer_limits, if_exists: true end - end - if current_adapter?(:Mysql2Adapter) def test_out_of_range_text_limit_should_raise - e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + e = assert_raise(ArgumentError) do Person.connection.create_table :test_text_limits, force: true do |t| t.text :bigtext, limit: 0xfffffffff end end - assert_match(/No text type has byte length #{0xfffffffff}/, e.message) + assert_includes e.message, "No text type has byte size #{0xfffffffff}" ensure Person.connection.drop_table :test_text_limits, if_exists: true end + + def test_out_of_range_binary_limit_should_raise + e = assert_raise(ArgumentError) do + Person.connection.create_table :test_binary_limits, force: true do |t| + t.binary :bigbinary, limit: 0xfffffffff + end + end + + assert_includes e.message, "No binary type has byte size #{0xfffffffff}" + ensure + Person.connection.drop_table :test_binary_limits, if_exists: true + end + end + + if current_adapter?(:Mysql2Adapter) + def test_invalid_text_size_should_raise + e = assert_raise(ArgumentError) do + Person.connection.create_table :test_text_sizes, force: true do |t| + t.text :bigtext, size: 0xfffffffff + end + end + + assert_equal "#{0xfffffffff} is invalid :size value. Only :tiny, :medium, and :long are allowed.", e.message + ensure + Person.connection.drop_table :test_text_sizes, if_exists: true + end end if ActiveRecord::Base.connection.supports_advisory_locks? def test_migrator_generates_valid_lock_id migration = Class.new(ActiveRecord::Migration::Current).new - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 100) lock_id = migrator.send(:generate_migrator_advisory_lock_id) @@ -653,7 +654,7 @@ class MigrationTest < ActiveRecord::TestCase # It is important we are consistent with how we generate this so that # exclusive locking works across migrator versions migration = Class.new(ActiveRecord::Migration::Current).new - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 100) lock_id = migrator.send(:generate_migrator_advisory_lock_id) @@ -675,7 +676,7 @@ class MigrationTest < ActiveRecord::TestCase end }.new - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 100) lock_id = migrator.send(:generate_migrator_advisory_lock_id) with_another_process_holding_lock(lock_id) do @@ -696,7 +697,7 @@ class MigrationTest < ActiveRecord::TestCase end }.new - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 100) lock_id = migrator.send(:generate_migrator_advisory_lock_id) with_another_process_holding_lock(lock_id) do @@ -709,7 +710,7 @@ class MigrationTest < ActiveRecord::TestCase def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock migration = Class.new(ActiveRecord::Migration::Current).new - migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + migrator = ActiveRecord::Migrator.new(:up, [migration], @schema_migration, 100) lock_id = migrator.send(:generate_migrator_advisory_lock_id) e = assert_raises(ActiveRecord::ConcurrentMigrationError) do @@ -742,15 +743,13 @@ class MigrationTest < ActiveRecord::TestCase test_terminated = Concurrent::CountDownLatch.new other_process = Thread.new do - begin - conn = ActiveRecord::Base.connection_pool.checkout - conn.get_advisory_lock(lock_id) - thread_lock.count_down - test_terminated.wait # hold the lock open until we tested everything - ensure - conn.release_advisory_lock(lock_id) - ActiveRecord::Base.connection_pool.checkin(conn) - end + conn = ActiveRecord::Base.connection_pool.checkout + conn.get_advisory_lock(lock_id) + thread_lock.count_down + test_terminated.wait # hold the lock open until we tested everything + ensure + conn.release_advisory_lock(lock_id) + ActiveRecord::Base.connection_pool.checkin(conn) end thread_lock.wait # wait until the 'other process' has the lock @@ -859,7 +858,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { - "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not + "Mysql2Adapter" => 1, # mysql2 supports creating two indexes using one statement "PostgreSQLAdapter" => 2, }.fetch(classname) { raise "need an expected query count for #{classname}" @@ -891,7 +890,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] expected_query_count = { - "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not + "Mysql2Adapter" => 1, # mysql2 supports dropping and creating two indexes using one statement "PostgreSQLAdapter" => 2, }.fetch(classname) { raise "need an expected query count for #{classname}" @@ -940,7 +939,6 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end private - def with_bulk_change_table # Reset columns/indexes cache as we're changing the table @columns = @indexes = nil diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 30e199f1c5..aeba8e1d14 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -23,8 +23,9 @@ class MigratorTest < ActiveRecord::TestCase def setup super - ActiveRecord::SchemaMigration.create_table - ActiveRecord::SchemaMigration.delete_all rescue nil + @schema_migration = ActiveRecord::Base.connection.schema_migration + @schema_migration.create_table + @schema_migration.delete_all rescue nil @verbose_was = ActiveRecord::Migration.verbose ActiveRecord::Migration.message_count = 0 ActiveRecord::Migration.class_eval do @@ -36,7 +37,7 @@ class MigratorTest < ActiveRecord::TestCase end teardown do - ActiveRecord::SchemaMigration.delete_all rescue nil + @schema_migration.delete_all rescue nil ActiveRecord::Migration.verbose = @verbose_was ActiveRecord::Migration.class_eval do undef :puts @@ -49,7 +50,7 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_with_duplicate_names e = assert_raises(ActiveRecord::DuplicateMigrationNameError) do list = [ActiveRecord::Migration.new("Chunky"), ActiveRecord::Migration.new("Chunky")] - ActiveRecord::Migrator.new(:up, list) + ActiveRecord::Migrator.new(:up, list, @schema_migration) end assert_match(/Multiple migrations have the name Chunky/, e.message) end @@ -57,39 +58,40 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_with_duplicate_versions assert_raises(ActiveRecord::DuplicateMigrationVersionError) do list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 1)] - ActiveRecord::Migrator.new(:up, list) + ActiveRecord::Migrator.new(:up, list, @schema_migration) end end def test_migrator_with_missing_version_numbers assert_raises(ActiveRecord::UnknownMigrationVersionError) do list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] - ActiveRecord::Migrator.new(:up, list, 3).run + ActiveRecord::Migrator.new(:up, list, @schema_migration, 3).run end assert_raises(ActiveRecord::UnknownMigrationVersionError) do list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] - ActiveRecord::Migrator.new(:up, list, -1).run + ActiveRecord::Migrator.new(:up, list, @schema_migration, -1).run end assert_raises(ActiveRecord::UnknownMigrationVersionError) do list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] - ActiveRecord::Migrator.new(:up, list, 0).run + ActiveRecord::Migrator.new(:up, list, @schema_migration, 0).run end assert_raises(ActiveRecord::UnknownMigrationVersionError) do list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] - ActiveRecord::Migrator.new(:up, list, 3).migrate + ActiveRecord::Migrator.new(:up, list, @schema_migration, 3).migrate end assert_raises(ActiveRecord::UnknownMigrationVersionError) do list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)] - ActiveRecord::Migrator.new(:up, list, -1).migrate + ActiveRecord::Migrator.new(:up, list, @schema_migration, -1).migrate end end def test_finds_migrations - migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid").migrations + schema_migration = ActiveRecord::Base.connection.schema_migration + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid", schema_migration).migrations [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first @@ -98,7 +100,8 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_subdirectories - migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations + schema_migration = ActiveRecord::Base.connection.schema_migration + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories", schema_migration).migrations [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first @@ -107,8 +110,9 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_from_two_directories + schema_migration = ActiveRecord::Base.connection.schema_migration directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"] - migrations = ActiveRecord::MigrationContext.new(directories).migrations + migrations = ActiveRecord::MigrationContext.new(directories, schema_migration).migrations [[20090101010101, "PeopleHaveHobbies"], [20090101010202, "PeopleHaveDescriptions"], @@ -121,14 +125,16 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_migrations_in_numbered_directory - migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban").migrations + schema_migration = ActiveRecord::Base.connection.schema_migration + migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban", schema_migration).migrations assert_equal 9, migrations[0].version assert_equal "AddExpressions", migrations[0].name end def test_relative_migrations + schema_migration = ActiveRecord::Base.connection.schema_migration list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::MigrationContext.new("valid").migrations + ActiveRecord::MigrationContext.new("valid", schema_migration).migrations end migration_proxy = list.find { |item| @@ -138,9 +144,9 @@ class MigratorTest < ActiveRecord::TestCase end def test_finds_pending_migrations - ActiveRecord::SchemaMigration.create!(version: "1") + @schema_migration.create!(version: "1") migration_list = [ActiveRecord::Migration.new("foo", 1), ActiveRecord::Migration.new("bar", 3)] - migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations + migrations = ActiveRecord::Migrator.new(:up, migration_list, @schema_migration).pending_migrations assert_equal 1, migrations.size assert_equal migration_list.last, migrations.first @@ -148,35 +154,38 @@ class MigratorTest < ActiveRecord::TestCase def test_migrations_status path = MIGRATIONS_ROOT + "/valid" + schema_migration = ActiveRecord::Base.connection.schema_migration - ActiveRecord::SchemaMigration.create(version: 2) - ActiveRecord::SchemaMigration.create(version: 10) + @schema_migration.create(version: 2) + @schema_migration.create(version: 10) assert_equal [ ["down", "001", "Valid people have last names"], ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::MigrationContext.new(path).migrations_status + ], ActiveRecord::MigrationContext.new(path, schema_migration).migrations_status end def test_migrations_status_in_subdirectories path = MIGRATIONS_ROOT + "/valid_with_subdirectories" + schema_migration = ActiveRecord::Base.connection.schema_migration - ActiveRecord::SchemaMigration.create(version: 2) - ActiveRecord::SchemaMigration.create(version: 10) + @schema_migration.create(version: 2) + @schema_migration.create(version: 10) assert_equal [ ["down", "001", "Valid people have last names"], ["up", "002", "We need reminders"], ["down", "003", "Innocent jointable"], ["up", "010", "********** NO FILE **********"], - ], ActiveRecord::MigrationContext.new(path).migrations_status + ], ActiveRecord::MigrationContext.new(path, schema_migration).migrations_status end def test_migrations_status_with_schema_define_in_subdirectories path = MIGRATIONS_ROOT + "/valid_with_subdirectories" prev_paths = ActiveRecord::Migrator.migrations_paths + schema_migration = ActiveRecord::Base.connection.schema_migration ActiveRecord::Migrator.migrations_paths = path ActiveRecord::Schema.define(version: 3) do @@ -186,16 +195,17 @@ class MigratorTest < ActiveRecord::TestCase ["up", "001", "Valid people have last names"], ["up", "002", "We need reminders"], ["up", "003", "Innocent jointable"], - ], ActiveRecord::MigrationContext.new(path).migrations_status + ], ActiveRecord::MigrationContext.new(path, schema_migration).migrations_status ensure ActiveRecord::Migrator.migrations_paths = prev_paths end def test_migrations_status_from_two_directories paths = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"] + schema_migration = ActiveRecord::Base.connection.schema_migration - ActiveRecord::SchemaMigration.create(version: "20100101010101") - ActiveRecord::SchemaMigration.create(version: "20160528010101") + @schema_migration.create(version: "20100101010101") + @schema_migration.create(version: "20160528010101") assert_equal [ ["down", "20090101010101", "People have hobbies"], @@ -204,18 +214,18 @@ class MigratorTest < ActiveRecord::TestCase ["down", "20100201010101", "Valid with timestamps we need reminders"], ["down", "20100301010101", "Valid with timestamps innocent jointable"], ["up", "20160528010101", "********** NO FILE **********"], - ], ActiveRecord::MigrationContext.new(paths).migrations_status + ], ActiveRecord::MigrationContext.new(paths, schema_migration).migrations_status end def test_migrator_interleaved_migrations pass_one = [Sensor.new("One", 1)] - ActiveRecord::Migrator.new(:up, pass_one).migrate + ActiveRecord::Migrator.new(:up, pass_one, @schema_migration).migrate assert pass_one.first.went_up assert_not pass_one.first.went_down pass_two = [Sensor.new("One", 1), Sensor.new("Three", 3)] - ActiveRecord::Migrator.new(:up, pass_two).migrate + ActiveRecord::Migrator.new(:up, pass_two, @schema_migration).migrate assert_not pass_two[0].went_up assert pass_two[1].went_up assert pass_two.all? { |x| !x.went_down } @@ -224,7 +234,7 @@ class MigratorTest < ActiveRecord::TestCase Sensor.new("Two", 2), Sensor.new("Three", 3)] - ActiveRecord::Migrator.new(:down, pass_three).migrate + ActiveRecord::Migrator.new(:down, pass_three, @schema_migration).migrate assert pass_three[0].went_down assert_not pass_three[1].went_down assert pass_three[2].went_down @@ -232,7 +242,7 @@ class MigratorTest < ActiveRecord::TestCase def test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - migrator = ActiveRecord::Migrator.new(:up, migrations) + migrator = ActiveRecord::Migrator.new(:up, migrations, @schema_migration) migrator.migrate assert migrations.all?(&:went_up) assert migrations.all? { |m| !m.went_down } @@ -243,7 +253,7 @@ class MigratorTest < ActiveRecord::TestCase test_up_calls_up migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - migrator = ActiveRecord::Migrator.new(:down, migrations) + migrator = ActiveRecord::Migrator.new(:down, migrations, @schema_migration) migrator.migrate assert migrations.all? { |m| !m.went_up } assert migrations.all?(&:went_down) @@ -251,30 +261,31 @@ class MigratorTest < ActiveRecord::TestCase end def test_current_version - ActiveRecord::SchemaMigration.create!(version: "1000") - migrator = ActiveRecord::MigrationContext.new("db/migrate") + @schema_migration.create!(version: "1000") + schema_migration = ActiveRecord::Base.connection.schema_migration + migrator = ActiveRecord::MigrationContext.new("db/migrate", schema_migration) assert_equal 1000, migrator.current_version end def test_migrator_one_up calls, migrations = sensors(3) - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 1).migrate assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:up, migrations, 2).migrate + ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 2).migrate assert_equal [[:up, 2]], calls end def test_migrator_one_down calls, migrations = sensors(3) - ActiveRecord::Migrator.new(:up, migrations).migrate + ActiveRecord::Migrator.new(:up, migrations, @schema_migration).migrate assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).migrate + ActiveRecord::Migrator.new(:down, migrations, @schema_migration, 1).migrate assert_equal [[:down, 3], [:down, 2]], calls end @@ -282,17 +293,17 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_one_up_one_down calls, migrations = sensors(3) - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 1).migrate assert_equal [[:up, 1]], calls calls.clear - ActiveRecord::Migrator.new(:down, migrations, 0).migrate + ActiveRecord::Migrator.new(:down, migrations, @schema_migration, 0).migrate assert_equal [[:down, 1]], calls end def test_migrator_double_up calls, migrations = sensors(3) - migrator = ActiveRecord::Migrator.new(:up, migrations, 1) + migrator = ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 1) assert_equal(0, migrator.current_version) migrator.migrate @@ -305,7 +316,7 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_double_down calls, migrations = sensors(3) - migrator = ActiveRecord::Migrator.new(:up, migrations, 1) + migrator = ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 1) assert_equal 0, migrator.current_version @@ -313,7 +324,7 @@ class MigratorTest < ActiveRecord::TestCase assert_equal [[:up, 1]], calls calls.clear - migrator = ActiveRecord::Migrator.new(:down, migrations, 1) + migrator = ActiveRecord::Migrator.new(:down, migrations, @schema_migration, 1) migrator.run assert_equal [[:down, 1]], calls calls.clear @@ -328,12 +339,12 @@ class MigratorTest < ActiveRecord::TestCase _, migrations = sensors(3) ActiveRecord::Migration.verbose = true - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 1).migrate assert_not_equal 0, ActiveRecord::Migration.message_count ActiveRecord::Migration.message_count = 0 - ActiveRecord::Migrator.new(:down, migrations, 0).migrate + ActiveRecord::Migrator.new(:down, migrations, @schema_migration, 0).migrate assert_not_equal 0, ActiveRecord::Migration.message_count end @@ -341,9 +352,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrations = sensors(3) ActiveRecord::Migration.verbose = false - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 1).migrate assert_equal 0, ActiveRecord::Migration.message_count - ActiveRecord::Migrator.new(:down, migrations, 0).migrate + ActiveRecord::Migrator.new(:down, migrations, @schema_migration, 0).migrate assert_equal 0, ActiveRecord::Migration.message_count end @@ -351,23 +362,24 @@ class MigratorTest < ActiveRecord::TestCase calls, migrations = sensors(3) # migrate up to 1 - ActiveRecord::Migrator.new(:up, migrations, 1).migrate + ActiveRecord::Migrator.new(:up, migrations, @schema_migration, 1).migrate assert_equal [[:up, 1]], calls calls.clear # migrate down to 0 - ActiveRecord::Migrator.new(:down, migrations, 0).migrate + ActiveRecord::Migrator.new(:down, migrations, @schema_migration, 0).migrate assert_equal [[:down, 1]], calls calls.clear # migrate down to 0 again - ActiveRecord::Migrator.new(:down, migrations, 0).migrate + ActiveRecord::Migrator.new(:down, migrations, @schema_migration, 0).migrate assert_equal [], calls end def test_migrator_going_down_due_to_version_target + schema_migration = ActiveRecord::Base.connection.schema_migration calls, migrator = migrator_class(3) - migrator = migrator.new("valid") + migrator = migrator.new("valid", schema_migration) migrator.up(1) assert_equal [[:up, 1]], calls @@ -382,8 +394,9 @@ class MigratorTest < ActiveRecord::TestCase end def test_migrator_output_when_running_multiple_migrations + schema_migration = ActiveRecord::Base.connection.schema_migration _, migrator = migrator_class(3) - migrator = migrator.new("valid") + migrator = migrator.new("valid", schema_migration) result = migrator.migrate assert_equal(3, result.count) @@ -397,8 +410,9 @@ class MigratorTest < ActiveRecord::TestCase end def test_migrator_output_when_running_single_migration + schema_migration = ActiveRecord::Base.connection.schema_migration _, migrator = migrator_class(1) - migrator = migrator.new("valid") + migrator = migrator.new("valid", schema_migration) result = migrator.run(:up, 1) @@ -406,8 +420,9 @@ class MigratorTest < ActiveRecord::TestCase end def test_migrator_rollback + schema_migration = ActiveRecord::Base.connection.schema_migration _, migrator = migrator_class(3) - migrator = migrator.new("valid") + migrator = migrator.new("valid", schema_migration) migrator.migrate assert_equal(3, migrator.current_version) @@ -426,18 +441,20 @@ class MigratorTest < ActiveRecord::TestCase end def test_migrator_db_has_no_schema_migrations_table + schema_migration = ActiveRecord::Base.connection.schema_migration _, migrator = migrator_class(3) - migrator = migrator.new("valid") + migrator = migrator.new("valid", schema_migration) - ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") + ActiveRecord::SchemaMigration.drop_table + assert_not_predicate ActiveRecord::SchemaMigration, :table_exists? migrator.migrate(1) - assert ActiveRecord::Base.connection.table_exists?("schema_migrations") + assert_predicate ActiveRecord::SchemaMigration, :table_exists? end def test_migrator_forward + schema_migration = ActiveRecord::Base.connection.schema_migration _, migrator = migrator_class(3) - migrator = migrator.new("/valid") + migrator = migrator.new("/valid", schema_migration) migrator.migrate(1) assert_equal(1, migrator.current_version) @@ -450,18 +467,20 @@ class MigratorTest < ActiveRecord::TestCase def test_only_loads_pending_migrations # migrate up to 1 - ActiveRecord::SchemaMigration.create!(version: "1") + @schema_migration.create!(version: "1") + schema_migration = ActiveRecord::Base.connection.schema_migration calls, migrator = migrator_class(3) - migrator = migrator.new("valid") + migrator = migrator.new("valid", schema_migration) migrator.migrate assert_equal [[:up, 2], [:up, 3]], calls end def test_get_all_versions + schema_migration = ActiveRecord::Base.connection.schema_migration _, migrator = migrator_class(3) - migrator = migrator.new("valid") + migrator = migrator.new("valid", schema_migration) migrator.migrate assert_equal([1, 2, 3], migrator.get_all_versions) diff --git a/activerecord/test/cases/multi_db_migrator_test.rb b/activerecord/test/cases/multi_db_migrator_test.rb new file mode 100644 index 0000000000..650b3af6f0 --- /dev/null +++ b/activerecord/test/cases/multi_db_migrator_test.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +require "cases/helper" +require "cases/migration/helper" + +class MultiDbMigratorTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + # Use this class to sense if migrations have gone + # up or down. + class Sensor < ActiveRecord::Migration::Current + attr_reader :went_up, :went_down + + def initialize(name = self.class.name, version = nil) + super + @went_up = false + @went_down = false + end + + def up; @went_up = true; end + def down; @went_down = true; end + end + + def setup + super + @connection_a = ActiveRecord::Base.connection + @connection_b = ARUnit2Model.connection + + @connection_a.schema_migration.create_table + @connection_b.schema_migration.create_table + + @connection_a.schema_migration.delete_all rescue nil + @connection_b.schema_migration.delete_all rescue nil + + @path_a = MIGRATIONS_ROOT + "/valid" + @path_b = MIGRATIONS_ROOT + "/to_copy" + + @schema_migration_a = @connection_a.schema_migration + @migrations_a = ActiveRecord::MigrationContext.new(@path_a, @schema_migration_a).migrations + @schema_migration_b = @connection_b.schema_migration + @migrations_b = ActiveRecord::MigrationContext.new(@path_b, @schema_migration_b).migrations + + @migrations_a_list = [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]] + @migrations_b_list = [[1, "PeopleHaveHobbies"], [2, "PeopleHaveDescriptions"]] + + @verbose_was = ActiveRecord::Migration.verbose + + ActiveRecord::Migration.message_count = 0 + ActiveRecord::Migration.class_eval do + undef :puts + def puts(*) + ActiveRecord::Migration.message_count += 1 + end + end + end + + teardown do + @connection_a.schema_migration.delete_all rescue nil + @connection_b.schema_migration.delete_all rescue nil + + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::Migration.class_eval do + undef :puts + def puts(*) + super + end + end + end + + def test_finds_migrations + @migrations_a_list.each_with_index do |pair, i| + assert_equal @migrations_a[i].version, pair.first + assert_equal @migrations_a[i].name, pair.last + end + + @migrations_b_list.each_with_index do |pair, i| + assert_equal @migrations_b[i].version, pair.first + assert_equal @migrations_b[i].name, pair.last + end + end + + def test_migrations_status + @schema_migration_a.create(version: 2) + @schema_migration_a.create(version: 10) + + assert_equal [ + ["down", "001", "Valid people have last names"], + ["up", "002", "We need reminders"], + ["down", "003", "Innocent jointable"], + ["up", "010", "********** NO FILE **********"], + ], ActiveRecord::MigrationContext.new(@path_a, @schema_migration_a).migrations_status + + @schema_migration_b.create(version: 4) + + assert_equal [ + ["down", "001", "People have hobbies"], + ["down", "002", "People have descriptions"], + ["up", "004", "********** NO FILE **********"] + ], ActiveRecord::MigrationContext.new(@path_b, @schema_migration_b).migrations_status + end + + def test_get_all_versions + _, migrator_a = migrator_class(3) + migrator_a = migrator_a.new(@path_a, @schema_migration_a) + + migrator_a.migrate + assert_equal([1, 2, 3], migrator_a.get_all_versions) + + migrator_a.rollback + assert_equal([1, 2], migrator_a.get_all_versions) + + migrator_a.rollback + assert_equal([1], migrator_a.get_all_versions) + + migrator_a.rollback + assert_equal([], migrator_a.get_all_versions) + + _, migrator_b = migrator_class(2) + migrator_b = migrator_b.new(@path_b, @schema_migration_b) + + migrator_b.migrate + assert_equal([1, 2], migrator_b.get_all_versions) + + migrator_b.rollback + assert_equal([1], migrator_b.get_all_versions) + + migrator_b.rollback + assert_equal([], migrator_b.get_all_versions) + end + + def test_finds_pending_migrations + @schema_migration_a.create!(version: "1") + migration_list_a = [ActiveRecord::Migration.new("foo", 1), ActiveRecord::Migration.new("bar", 3)] + migrations_a = ActiveRecord::Migrator.new(:up, migration_list_a, @schema_migration_a).pending_migrations + + assert_equal 1, migrations_a.size + assert_equal migration_list_a.last, migrations_a.first + + @schema_migration_b.create!(version: "1") + migration_list_b = [ActiveRecord::Migration.new("foo", 1), ActiveRecord::Migration.new("bar", 3)] + migrations_b = ActiveRecord::Migrator.new(:up, migration_list_b, @schema_migration_b).pending_migrations + + assert_equal 1, migrations_b.size + assert_equal migration_list_b.last, migrations_b.first + end + + def test_migrator_db_has_no_schema_migrations_table + _, migrator = migrator_class(3) + migrator = migrator.new(@path_a, @schema_migration_a) + + @schema_migration_a.drop_table + assert_not @connection_a.table_exists?("schema_migrations") + migrator.migrate(1) + assert @connection_a.table_exists?("schema_migrations") + + _, migrator = migrator_class(3) + migrator = migrator.new(@path_b, @schema_migration_b) + + @schema_migration_b.drop_table + assert_not @connection_b.table_exists?("schema_migrations") + migrator.migrate(1) + assert @connection_b.table_exists?("schema_migrations") + end + + def test_migrator_forward + _, migrator = migrator_class(3) + migrator = migrator.new(@path_a, @schema_migration_a) + migrator.migrate(1) + assert_equal(1, migrator.current_version) + + migrator.forward(2) + assert_equal(3, migrator.current_version) + + migrator.forward + assert_equal(3, migrator.current_version) + + _, migrator_b = migrator_class(3) + migrator_b = migrator_b.new(@path_b, @schema_migration_b) + migrator_b.migrate(1) + assert_equal(1, migrator_b.current_version) + + migrator_b.forward(2) + assert_equal(3, migrator_b.current_version) + + migrator_b.forward + assert_equal(3, migrator_b.current_version) + end + + private + def m(name, version) + x = Sensor.new name, version + x.extend(Module.new { + define_method(:up) { yield(:up, x); super() } + define_method(:down) { yield(:down, x); super() } + }) if block_given? + end + + def sensors(count) + calls = [] + migrations = count.times.map { |i| + m(nil, i + 1) { |c, migration| + calls << [c, migration.version] + } + } + [calls, migrations] + end + + def migrator_class(count) + calls, migrations = sensors(count) + + migrator = Class.new(ActiveRecord::MigrationContext) { + define_method(:migrations) { |*| + migrations + } + } + [calls, migrator] + end +end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 192d2f5251..f11c441c65 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -106,14 +106,12 @@ class MultipleDbTest < ActiveRecord::TestCase end def test_associations_should_work_when_model_has_no_connection - begin - ActiveRecord::Base.remove_connection - assert_nothing_raised do - College.first.courses.first - end - ensure - ActiveRecord::Base.establish_connection :arunit + ActiveRecord::Base.remove_connection + assert_nothing_raised do + College.first.courses.first end + ensure + ActiveRecord::Base.establish_connection :arunit end end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index bb1c1ea17d..b49e62bee6 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -851,7 +851,6 @@ module NestedAttributesOnACollectionAssociationTests end private - def association_setter @association_setter ||= "#{@association_name}_attributes=".to_sym end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 4830ff2b5f..7b7aa7e9b7 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -53,6 +53,20 @@ class PersistenceTest < ActiveRecord::TestCase assert_not_equal "2 updated", Topic.find(2).content end + def test_class_level_update_without_ids + topics = Topic.all + assert_equal 5, topics.length + topics.each do |topic| + assert_not_equal "updated", topic.content + end + + updated = Topic.update(content: "updated") + assert_equal 5, updated.length + updated.each do |topic| + assert_equal "updated", topic.content + end + end + def test_class_level_update_is_affected_by_scoping topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } @@ -169,7 +183,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_not_predicate company, :valid? original_errors = company.errors client = company.becomes(Client) - assert_equal original_errors.keys, client.errors.keys + assert_equal assert_deprecated { original_errors.keys }, assert_deprecated { client.errors.keys } end def test_becomes_errors_base @@ -183,7 +197,7 @@ class PersistenceTest < ActiveRecord::TestCase admin.errors.add :token, :invalid child = admin.becomes(child_class) - assert_equal [:token], child.errors.keys + assert_equal [:token], assert_deprecated { child.errors.keys } assert_nothing_raised do child.errors.add :foo, :invalid end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index fa7f759e51..d783b2945d 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -25,14 +25,12 @@ class PooledConnectionsTest < ActiveRecord::TestCase @timed_out = 0 threads.times do Thread.new do - begin - conn = ActiveRecord::Base.connection_pool.checkout - sleep 0.1 - ActiveRecord::Base.connection_pool.checkin conn - @connection_count += 1 - rescue ActiveRecord::ConnectionTimeoutError - @timed_out += 1 - end + conn = ActiveRecord::Base.connection_pool.checkout + sleep 0.1 + ActiveRecord::Base.connection_pool.checkin conn + @connection_count += 1 + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 end.join end end @@ -42,14 +40,12 @@ class PooledConnectionsTest < ActiveRecord::TestCase @connection_count = 0 @timed_out = 0 loops.times do - begin - conn = ActiveRecord::Base.connection_pool.checkout - ActiveRecord::Base.connection_pool.checkin conn - @connection_count += 1 - ActiveRecord::Base.connection.data_sources - rescue ActiveRecord::ConnectionTimeoutError - @timed_out += 1 - end + conn = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.checkin conn + @connection_count += 1 + ActiveRecord::Base.connection.data_sources + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 end end @@ -76,7 +72,6 @@ class PooledConnectionsTest < ActiveRecord::TestCase end private - def add_record(name) ActiveRecord::Base.connection_pool.with_connection { Project.create! name: name } end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4ed7469039..511d7fc982 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -203,6 +203,14 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_queries(3, ignore_none: true) { klass.create! } end + def test_assign_id_raises_error_if_primary_key_doesnt_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = "dashboards" + end + dashboard = klass.new + assert_raises(ActiveModel::MissingAttributeError) { dashboard.id = "1" } + end + if current_adapter?(:PostgreSQLAdapter) def test_serial_with_quoted_sequence_name column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] @@ -354,7 +362,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end def test_composite_primary_key_out_of_order - skip if current_adapter?(:SQLite3Adapter) assert_equal ["code", "region"], @connection.primary_keys("barcodes_reverse") end @@ -376,7 +383,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end def test_dumping_composite_primary_key_out_of_order - skip if current_adapter?(:SQLite3Adapter) schema = dump_table_schema "barcodes_reverse" assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 565190c476..79bd6906d1 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -55,78 +55,97 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off end + def test_query_cache_is_applied_to_connections_in_all_handlers + ActiveRecord::Base.connection_handlers = { + writing: ActiveRecord::Base.default_connection_handler, + reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new + } + + ActiveRecord::Base.connected_to(role: :reading) do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + mw = middleware { |env| + ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection + assert_predicate ActiveRecord::Base.connection, :query_cache_enabled + assert_predicate ro_conn, :query_cache_enabled + } + + mw.call({}) + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + def test_query_cache_across_threads with_temporary_connection_pool do - begin - if in_memory_db? - # Separate connections to an in-memory database create an entirely new database, - # with an empty schema etc, so we just stub out this schema on the fly. - ActiveRecord::Base.connection_pool.with_connection do |connection| - connection.create_table :tasks do |t| - t.datetime :starting - t.datetime :ending - end + if in_memory_db? + # Separate connections to an in-memory database create an entirely new database, + # with an empty schema etc, so we just stub out this schema on the fly. + ActiveRecord::Base.connection_pool.with_connection do |connection| + connection.create_table :tasks do |t| + t.datetime :starting + t.datetime :ending end - ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) end + ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) + end - ActiveRecord::Base.connection_pool.connections.each do |conn| - assert_cache :off, conn - end + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end - assert_not_predicate ActiveRecord::Base.connection, :nil? - assert_cache :off + assert_not_predicate ActiveRecord::Base.connection, :nil? + assert_cache :off - middleware { - assert_cache :clean + middleware { + assert_cache :clean - Task.find 1 - assert_cache :dirty + Task.find 1 + assert_cache :dirty - thread_1_connection = ActiveRecord::Base.connection - ActiveRecord::Base.clear_active_connections! - assert_cache :off, thread_1_connection + thread_1_connection = ActiveRecord::Base.connection + ActiveRecord::Base.clear_active_connections! + assert_cache :off, thread_1_connection - started = Concurrent::Event.new - checked = Concurrent::Event.new + started = Concurrent::Event.new + checked = Concurrent::Event.new - thread_2_connection = nil - thread = Thread.new { - thread_2_connection = ActiveRecord::Base.connection + thread_2_connection = nil + thread = Thread.new { + thread_2_connection = ActiveRecord::Base.connection - assert_equal thread_2_connection, thread_1_connection - assert_cache :off + assert_equal thread_2_connection, thread_1_connection + assert_cache :off - middleware { - assert_cache :clean + middleware { + assert_cache :clean - Task.find 1 - assert_cache :dirty + Task.find 1 + assert_cache :dirty - started.set - checked.wait + started.set + checked.wait - ActiveRecord::Base.clear_active_connections! - }.call({}) - } + ActiveRecord::Base.clear_active_connections! + }.call({}) + } - started.wait + started.wait - thread_1_connection = ActiveRecord::Base.connection - assert_not_equal thread_1_connection, thread_2_connection - assert_cache :dirty, thread_2_connection - checked.set - thread.join + thread_1_connection = ActiveRecord::Base.connection + assert_not_equal thread_1_connection, thread_2_connection + assert_cache :dirty, thread_2_connection + checked.set + thread.join - assert_cache :off, thread_2_connection - }.call({}) + assert_cache :off, thread_2_connection + }.call({}) - ActiveRecord::Base.connection_pool.connections.each do |conn| - assert_cache :off, conn - end - ensure - ActiveRecord::Base.connection_pool.disconnect! + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn end + ensure + ActiveRecord::Base.connection_pool.disconnect! end end @@ -295,7 +314,7 @@ class QueryCacheTest < ActiveRecord::TestCase payload[:sql].downcase! end - assert_raises frozen_error_class do + assert_raises FrozenError do ActiveRecord::Base.cache do assert_queries(1) { Task.find(1); Task.find(1) } end @@ -316,11 +335,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_does_not_wrap_results_in_arrays Task.cache do - if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter) - assert_equal 2, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - else - assert_instance_of String, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - end + assert_equal 2, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") end end @@ -353,12 +368,10 @@ class QueryCacheTest < ActiveRecord::TestCase assert_not_predicate Task, :connected? Task.cache do - begin - assert_queries(1) { Task.find(1); Task.find(1) } - ensure - ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) - Task.connection_specification_name = spec_name - end + assert_queries(1) { Task.find(1); Task.find(1) } + ensure + ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) + Task.connection_specification_name = spec_name end end end @@ -485,8 +498,62 @@ class QueryCacheTest < ActiveRecord::TestCase }.call({}) end - private + def test_clear_query_cache_is_called_on_all_connections + skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db? + with_temporary_connection_pool do + ActiveRecord::Base.connection_handlers = { + writing: ActiveRecord::Base.default_connection_handler, + reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new + } + ActiveRecord::Base.connected_to(role: :reading) do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + mw = middleware { |env| + ActiveRecord::Base.connected_to(role: :reading) do + @topic = Topic.first + end + + assert @topic + + ActiveRecord::Base.connected_to(role: :writing) do + @topic.title = "It doesn't have to be crazy at work" + @topic.save! + end + + assert_equal "It doesn't have to be crazy at work", @topic.title + + ActiveRecord::Base.connected_to(role: :reading) do + @topic = Topic.first + assert_equal "It doesn't have to be crazy at work", @topic.title + end + } + + mw.call({}) + end + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + + test "query cache is enabled in threads with shared connection" do + ActiveRecord::Base.connection_pool.lock_thread = true + + assert_cache :off + + thread_a = Thread.new do + middleware { |env| + assert_cache :clean + [200, {}, nil] + }.call({}) + end + + thread_a.join + + ActiveRecord::Base.connection_pool.lock_thread = false + end + + private def with_temporary_connection_pool old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index b630f782bc..402ddcf05a 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -48,7 +48,7 @@ module ActiveRecord reaper = ConnectionPool::Reaper.new(fp, 0.0001) reaper.run - until fp.reaped + until fp.flushed Thread.pass end assert fp.reaped diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index a8030c2d64..085006c9a2 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/comment" module ActiveRecord - module ArrayDelegationTests + module DelegationTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, @@ -21,25 +21,14 @@ module ActiveRecord assert_respond_to target, method end end - end - - module DeprecatedArelDelegationTests - AREL_METHODS = [ - :with, :orders, :froms, :project, :projections, :taken, :constraints, :exists, :locked, :where_sql, - :ast, :source, :join_sources, :to_dot, :create_insert, :create_true, :create_false - ] - def test_deprecate_arel_delegation - AREL_METHODS.each do |method| - assert_deprecated { target.public_send(method) } - assert_deprecated { target.public_send(method) } - end + def test_not_respond_to_arel_method + assert_not_respond_to target, :exists end end class DelegationAssociationTest < ActiveRecord::TestCase - include ArrayDelegationTests - include DeprecatedArelDelegationTests + include DelegationTests def target Post.new.comments @@ -47,8 +36,7 @@ module ActiveRecord end class DelegationRelationTest < ActiveRecord::TestCase - include ArrayDelegationTests - include DeprecatedArelDelegationTests + include DelegationTests def target Comment.all @@ -56,26 +44,28 @@ module ActiveRecord end class QueryingMethodsDelegationTest < ActiveRecord::TestCase - QUERYING_METHODS = [ - :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, - :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, - :first_or_create, :first_or_create!, :first_or_initialize, - :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, - :find_by, :find_by!, - :destroy_all, :delete_all, :update_all, - :find_each, :find_in_batches, :in_batches, - :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, - :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, - :having, :create_with, :distinct, :references, :none, :unscope, :merge, - :count, :average, :minimum, :maximum, :sum, :calculate, - :pluck, :pick, :ids, - ] + QUERYING_METHODS = + ActiveRecord::Batches.public_instance_methods(false) + + ActiveRecord::Calculations.public_instance_methods(false) + + ActiveRecord::FinderMethods.public_instance_methods(false) - [:raise_record_not_found_exception!] + + ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] + + ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method| + method.to_s.end_with?("=", "!", "value", "values", "clause") + } - [:reverse_order, :arel, :extensions, :construct_join_dependency] + [ + :any?, :many?, :none?, :one?, + :first_or_create, :first_or_create!, :first_or_initialize, + :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, + :create_or_find_by, :create_or_find_by!, + :destroy_all, :delete_all, :update_all, :touch_all, :delete_by, :destroy_by + ] def test_delegate_querying_methods klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" end + assert_equal QUERYING_METHODS.sort, ActiveRecord::Querying::QUERYING_METHODS.sort + QUERYING_METHODS.each do |method| assert_respond_to klass.all, method assert_respond_to klass, method diff --git a/activerecord/test/cases/relation/delete_all_test.rb b/activerecord/test/cases/relation/delete_all_test.rb index 446d7621ea..d1c13fa1b5 100644 --- a/activerecord/test/cases/relation/delete_all_test.rb +++ b/activerecord/test/cases/relation/delete_all_test.rb @@ -80,25 +80,23 @@ class DeleteAllTest < ActiveRecord::TestCase assert_equal pets.count, pets.delete_all end - unless current_adapter?(:OracleAdapter) - def test_delete_all_with_order_and_limit_deletes_subset_only - author = authors(:david) - limited_posts = Post.where(author: author).order(:id).limit(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.delete_all - assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) } - assert posts(:thinking) - end + def test_delete_all_with_order_and_limit_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) } + assert posts(:thinking) + end - def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only - author = authors(:david) - limited_posts = Post.where(author: author).order(:id).limit(1).offset(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.delete_all - assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) } - assert posts(:welcome) - end + def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1).offset(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) } + assert posts(:welcome) end end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 224e4f39a8..5c5e760e34 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -135,6 +135,18 @@ class RelationMergingTest < ActiveRecord::TestCase relation = Post.all.merge(Post.order(Arel.sql("title LIKE '%?'"))) assert_equal ["title LIKE '%?'"], relation.order_values end + + def test_merging_annotations_respects_merge_order + assert_sql(%r{/\* foo \*/ /\* bar \*/}) do + Post.annotate("foo").merge(Post.annotate("bar")).first + end + assert_sql(%r{/\* bar \*/ /\* foo \*/}) do + Post.annotate("bar").merge(Post.annotate("foo")).first + end + assert_sql(%r{/\* foo \*/ /\* bar \*/ /\* baz \*/ /\* qux \*/}) do + Post.annotate("foo").annotate("bar").merge(Post.annotate("baz").annotate("qux")).first + end + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index f82ecd4449..96249b8d51 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -26,7 +26,7 @@ module ActiveRecord assert relation.order!(:name).equal?(relation) node = relation.order_values.first assert_predicate node, :ascending? - assert_equal :name, node.expr.name + assert_equal "name", node.expr.name assert_equal "posts", node.expr.relation.name end @@ -89,7 +89,7 @@ module ActiveRecord node = relation.order_values.first assert_predicate node, :ascending? - assert_equal :name, node.expr.name + assert_equal "name", node.expr.name assert_equal "posts", node.expr.relation.name end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 065819e0f1..8623867864 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -30,6 +30,11 @@ module ActiveRecord assert_equal expected, Post.where("id = 1").or(Post.none).to_a end + def test_or_with_large_number + expected = Post.where("id = 1 or id = 9223372036854775808").to_a + assert_equal expected, Post.where(id: 1).or(Post.where(id: 9223372036854775808)).to_a + end + def test_or_with_bind_params assert_equal Post.find([1, 2]).sort_by(&:id), Post.where(id: 1).or(Post.where(id: 2)).sort_by(&:id) end diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb index dec8a6925d..586aaadd0a 100644 --- a/activerecord/test/cases/relation/select_test.rb +++ b/activerecord/test/cases/relation/select_test.rb @@ -11,5 +11,17 @@ module ActiveRecord expected = Post.select(:title).to_sql assert_equal expected, Post.select(nil).select(:title).to_sql end + + def test_reselect + expected = Post.select(:title).to_sql + assert_equal expected, Post.select(:title, :body).reselect(:title).to_sql + end + + def test_reselect_with_default_scope_select + expected = Post.select(:title).to_sql + actual = PostWithDefaultSelect.reselect(:title).to_sql + + assert_equal expected, actual + end end end diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb index 09c365f31b..e45531b4a9 100644 --- a/activerecord/test/cases/relation/update_all_test.rb +++ b/activerecord/test/cases/relation/update_all_test.rb @@ -138,14 +138,6 @@ class UpdateAllTest < ActiveRecord::TestCase assert_equal new_time, developer.updated_at end - def test_touch_all_updates_locking_column - person = people(:david) - - assert_difference -> { person.reload.lock_version }, +1 do - Person.where(first_name: "David").touch_all - end - end - def test_update_on_relation topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil @@ -186,6 +178,101 @@ class UpdateAllTest < ActiveRecord::TestCase end end + def test_update_all_cares_about_optimistic_locking + david = people(:david) + + travel 5.seconds do + now = Time.now.utc + assert_not_equal now, david.updated_at + + people = Person.where(id: people(:michael, :david, :susan)) + expected = people.pluck(:lock_version) + expected.map! { |version| version + 1 } + people.update_all(updated_at: now) + + assert_equal [now] * 3, people.pluck(:updated_at) + assert_equal expected, people.pluck(:lock_version) + + assert_raises(ActiveRecord::StaleObjectError) do + david.touch(time: now) + end + end + end + + def test_update_counters_cares_about_optimistic_locking + david = people(:david) + + travel 5.seconds do + now = Time.now.utc + assert_not_equal now, david.updated_at + + people = Person.where(id: people(:michael, :david, :susan)) + expected = people.pluck(:lock_version) + expected.map! { |version| version + 1 } + people.update_counters(touch: [time: now]) + + assert_equal [now] * 3, people.pluck(:updated_at) + assert_equal expected, people.pluck(:lock_version) + + assert_raises(ActiveRecord::StaleObjectError) do + david.touch(time: now) + end + end + end + + def test_touch_all_cares_about_optimistic_locking + david = people(:david) + + travel 5.seconds do + now = Time.now.utc + assert_not_equal now, david.updated_at + + people = Person.where(id: people(:michael, :david, :susan)) + expected = people.pluck(:lock_version) + expected.map! { |version| version + 1 } + people.touch_all(time: now) + + assert_equal [now] * 3, people.pluck(:updated_at) + assert_equal expected, people.pluck(:lock_version) + + assert_raises(ActiveRecord::StaleObjectError) do + david.touch(time: now) + end + end + end + + def test_klass_level_update_all + travel 5.seconds do + now = Time.now.utc + + Person.all.each do |person| + assert_not_equal now, person.updated_at + end + + Person.update_all(updated_at: now) + + Person.all.each do |person| + assert_equal now, person.updated_at + end + end + end + + def test_klass_level_touch_all + travel 5.seconds do + now = Time.now.utc + + Person.all.each do |person| + assert_not_equal now, person.updated_at + end + + Person.touch_all(time: now) + + Person.all.each do |person| + assert_equal now, person.updated_at + end + end + end + # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) def test_update_all_ignores_order_without_limit_from_association @@ -198,11 +285,9 @@ class UpdateAllTest < ActiveRecord::TestCase def test_update_all_doesnt_ignore_order assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error test_update_with_order_succeeds = lambda do |order| - begin - Author.order(order).update_all("id = id + 1") - rescue ActiveRecord::ActiveRecordError - false - end + Author.order(order).update_all("id = id + 1") + rescue ActiveRecord::ActiveRecordError + false end if test_update_with_order_succeeds.call("id DESC") diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index 0b06cec40b..35db3d1175 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -106,7 +106,7 @@ class ActiveRecord::Relation Arel::Nodes::Not.new(random_object) ]) - assert_equal expected, original.invert + assert_equal expected, original.invert(:nor) end test "except removes binary predicates referencing a given column" do @@ -233,7 +233,6 @@ class ActiveRecord::Relation end private - def table Arel::Table.new("table") end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 99797528b2..aad30ddea0 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -14,10 +14,23 @@ require "models/price_estimate" require "models/topic" require "models/treasure" require "models/vertex" +require "support/stubs/strong_parameters" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates, :topics + fixtures :posts, :comments, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates, :topics + + def test_in_clause_is_correctly_sliced + assert_called(Author.connection, :in_clause_length, returns: 1) do + david = authors(:david) + assert_equal [david], Author.where(name: "David", id: [1, 2]) + end + end + + def test_type_casting_nested_joins + comment = comments(:eager_other_comment1) + assert_equal [comment], Comment.joins(post: :author).where(authors: { id: "2-foo" }) + end def test_where_copies_bind_params author = authors(:david) @@ -50,8 +63,13 @@ module ActiveRecord assert_equal [chef], chefs.to_a end - def test_where_with_casted_value_is_nil - assert_equal 4, Topic.where(last_read: "").count + def test_where_with_invalid_value + topics(:first).update!(parent_id: 0, written_on: nil, bonus_time: nil, last_read: nil) + assert_empty Topic.where(parent_id: Object.new) + assert_empty Topic.where(parent_id: "not-a-number") + assert_empty Topic.where(written_on: "") + assert_empty Topic.where(bonus_time: "") + assert_empty Topic.where(last_read: "") end def test_rewhere_on_root @@ -109,13 +127,58 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end - def test_polymorphic_shallow_where_not - treasure = treasures(:sapphire) + def test_where_not_polymorphic_association + sapphire = treasures(:sapphire) - expected = [price_estimates(:diamond), price_estimates(:honda)] - actual = PriceEstimate.where.not(estimate_of: treasure) + all = [treasures(:diamond), sapphire, cars(:honda), sapphire] + assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of) - assert_equal expected.sort_by(&:id), actual.sort_by(&:id) + actual = PriceEstimate.where.not(estimate_of: sapphire) + only = PriceEstimate.where(estimate_of: sapphire) + + expected = all - [sapphire] + assert_equal expected, actual.sort_by(&:id).map(&:estimate_of) + assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of) + end + + def test_where_not_polymorphic_id_and_type_as_nand + sapphire = treasures(:sapphire) + + all = [treasures(:diamond), sapphire, cars(:honda), sapphire] + assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of) + + actual = PriceEstimate.where.yield_self do |where_chain| + where_chain.stub(:not_behaves_as_nor?, false) do + where_chain.not(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + end + end + only = PriceEstimate.where(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + + expected = all - [sapphire] + assert_equal expected, actual.sort_by(&:id).map(&:estimate_of) + assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of) + end + + def test_where_not_polymorphic_id_and_type_as_nor_is_deprecated + sapphire = treasures(:sapphire) + + all = [treasures(:diamond), sapphire, cars(:honda), sapphire] + assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of) + + message = <<~MSG.squish + NOT conditions will no longer behave as NOR in Rails 6.1. + To continue using NOR conditions, NOT each conditions manually + (`.where.not(:estimate_of_type => ...).where.not(:estimate_of_id => ...)`). + MSG + actual = assert_deprecated(message) do + PriceEstimate.where.not(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + end + only = PriceEstimate.where(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + + expected = all - [sapphire] + # NOT (estimate_of_type = 'Treasure' OR estimate_of_id = sapphire.id) matches only `cars(:honda)` unfortunately. + assert_not_equal expected, actual.sort_by(&:id).map(&:estimate_of) + assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of) end def test_polymorphic_nested_array_where @@ -334,31 +397,22 @@ module ActiveRecord end def test_where_with_strong_parameters - protected_params = Class.new do - attr_reader :permitted - alias :permitted? :permitted - - def initialize(parameters) - @parameters = parameters - @permitted = false - end - - def to_h - @parameters - end - - def permit! - @permitted = true - self - end - end - author = authors(:david) - params = protected_params.new(name: author.name) + params = ProtectedParams.new(name: author.name) assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) } assert_equal author, Author.where(params.permit!).first end + def test_where_with_large_number + assert_equal [authors(:bob)], Author.where(id: [3, 9223372036854775808]) + assert_equal [authors(:bob)], Author.where(id: 3..9223372036854775808) + end + + def test_to_sql_with_large_number + assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: [3, 9223372036854775808]).to_sql) + assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: 3..9223372036854775808).to_sql) + end + def test_where_with_unsupported_arguments assert_raises(ArgumentError) { Author.where(42) } end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 68161f6a84..e74fb1a098 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -101,6 +101,9 @@ module ActiveRecord relation.merge!(relation) assert_predicate relation, :empty_scope? + + assert_not_predicate NullPost.all, :empty_scope? + assert_not_predicate FirstPost.all, :empty_scope? end def test_bad_constants_raise_errors @@ -289,6 +292,7 @@ module ActiveRecord klass.create!(description: "foo") assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc) + assert_equal ["foo"], klass.reselect(:description).from(klass.all).map(&:desc) end def test_relation_merging_with_merged_joins_as_strings @@ -307,6 +311,65 @@ module ActiveRecord assert_equal 3, ratings.count end + def test_relation_with_annotation_includes_comment_in_to_sql + post_with_annotation = Post.where(id: 1).annotate("foo") + assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql + end + + def test_relation_with_annotation_includes_comment_in_sql + post_with_annotation = Post.where(id: 1).annotate("foo") + assert_sql(%r{/\* foo \*/}) do + assert post_with_annotation.first, "record should be found" + end + end + + def test_relation_with_annotation_chains_sql_comments + post_with_annotation = Post.where(id: 1).annotate("foo").annotate("bar") + assert_sql(%r{/\* foo \*/ /\* bar \*/}) do + assert post_with_annotation.first, "record should be found" + end + end + + def test_relation_with_annotation_filters_sql_comment_delimiters + post_with_annotation = Post.where(id: 1).annotate("**//foo//**") + assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql + end + + def test_relation_with_annotation_includes_comment_in_count_query + post_with_annotation = Post.annotate("foo") + all_count = Post.all.to_a.count + assert_sql(%r{/\* foo \*/}) do + assert_equal all_count, post_with_annotation.count + end + end + + def test_relation_without_annotation_does_not_include_an_empty_comment + log = capture_sql do + Post.where(id: 1).first + end + + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty? + end + + def test_relation_with_optimizer_hints_filters_sql_comment_delimiters + post_with_hint = Post.where(id: 1).optimizer_hints("**//BADHINT//**") + assert_match %r{BADHINT}, post_with_hint.to_sql + assert_no_match %r{\*/BADHINT}, post_with_hint.to_sql + assert_no_match %r{\*//BADHINT}, post_with_hint.to_sql + assert_no_match %r{BADHINT/\*}, post_with_hint.to_sql + assert_no_match %r{BADHINT//\*}, post_with_hint.to_sql + post_with_hint = Post.where(id: 1).optimizer_hints("/*+ BADHINT */") + assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql + end + + def test_does_not_duplicate_optimizer_hints_on_merge + escaped_table = Post.connection.quote_table_name("posts") + expected = "SELECT /*+ OMGHINT */ #{escaped_table}.* FROM #{escaped_table}" + query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql + assert_equal expected, query + end + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value def type :string @@ -349,7 +412,6 @@ module ActiveRecord end private - def skip_if_sqlite3_version_includes_quoting_bug if sqlite3_version_includes_quoting_bug? skip <<-ERROR.squish diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index e471ee8039..1a20fe5dc2 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -14,6 +14,7 @@ require "models/person" require "models/computer" require "models/reply" require "models/company" +require "models/contract" require "models/bird" require "models/car" require "models/engine" @@ -181,15 +182,64 @@ class RelationTest < ActiveRecord::TestCase end end + def test_select_with_from_includes_original_table_name + relation = Comment.joins(:post).select(:id).order(:id) + subquery = Comment.from("#{Comment.table_name} /*! USE INDEX (PRIMARY) */").joins(:post).select(:id).order(:id) + assert_equal relation.map(&:id), subquery.map(&:id) + end + + def test_pluck_with_from_includes_original_table_name + relation = Comment.joins(:post).order(:id) + subquery = Comment.from("#{Comment.table_name} /*! USE INDEX (PRIMARY) */").joins(:post).order(:id) + assert_equal relation.pluck(:id), subquery.pluck(:id) + end + + def test_select_with_from_includes_quoted_original_table_name + relation = Comment.joins(:post).select(:id).order(:id) + subquery = Comment.from("#{Comment.quoted_table_name} /*! USE INDEX (PRIMARY) */").joins(:post).select(:id).order(:id) + assert_equal relation.map(&:id), subquery.map(&:id) + end + + def test_pluck_with_from_includes_quoted_original_table_name + relation = Comment.joins(:post).order(:id) + subquery = Comment.from("#{Comment.quoted_table_name} /*! USE INDEX (PRIMARY) */").joins(:post).order(:id) + assert_equal relation.pluck(:id), subquery.pluck(:id) + end + + def test_select_with_subquery_in_from_uses_original_table_name + relation = Comment.joins(:post).select(:id).order(:id) + # Avoid subquery flattening by adding distinct to work with SQLite < 3.20.0. + subquery = Comment.from(Comment.all.distinct, Comment.quoted_table_name).joins(:post).select(:id).order(:id) + assert_equal relation.map(&:id), subquery.map(&:id) + end + + def test_pluck_with_subquery_in_from_uses_original_table_name + relation = Comment.joins(:post).order(:id) + subquery = Comment.from(Comment.all, Comment.quoted_table_name).joins(:post).order(:id) + assert_equal relation.pluck(:id), subquery.pluck(:id) + end + def test_select_with_subquery_in_from_does_not_use_original_table_name relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type") - subquery = Comment.from(relation).select("type", "post_count") + subquery = Comment.from(relation, "grouped_#{Comment.table_name}").select("type", "post_count") assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort) end def test_group_with_subquery_in_from_does_not_use_original_table_name relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type") - subquery = Comment.from(relation).group("type").average("post_count") + subquery = Comment.from(relation, "grouped_#{Comment.table_name}").group("type").average("post_count") + assert_equal(relation.map(&:post_count).sort, subquery.values.sort) + end + + def test_select_with_subquery_string_in_from_does_not_use_original_table_name + relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type") + subquery = Comment.from("(#{relation.to_sql}) #{Comment.table_name}_grouped").select("type", "post_count") + assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort) + end + + def test_group_with_subquery_string_in_from_does_not_use_original_table_name + relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type") + subquery = Comment.from("(#{relation.to_sql}) #{Comment.table_name}_grouped").group("type").average("post_count") assert_equal(relation.map(&:post_count).sort, subquery.values.sort) end @@ -248,7 +298,7 @@ class RelationTest < ActiveRecord::TestCase end def test_reverse_order_with_function - topics = Topic.order(Arel.sql("length(title)")).reverse_order + topics = Topic.order("length(title)").reverse_order assert_equal topics(:second).title, topics.first.title end @@ -258,9 +308,9 @@ class RelationTest < ActiveRecord::TestCase end def test_reverse_order_with_function_other_predicates - topics = Topic.order(Arel.sql("author_name, length(title), id")).reverse_order + topics = Topic.order("author_name, length(title), id").reverse_order assert_equal topics(:second).title, topics.first.title - topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order + topics = Topic.order("length(author_name), id, length(title)").reverse_order assert_equal topics(:fifth).title, topics.first.title end @@ -287,12 +337,21 @@ class RelationTest < ActiveRecord::TestCase def test_reverse_order_with_nulls_first_or_last assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order(Arel.sql("title NULLS FIRST")).reverse_order + Topic.order("title NULLS FIRST").reverse_order end assert_raises(ActiveRecord::IrreversibleOrderError) do - Topic.order(Arel.sql("title nulls last")).reverse_order + Topic.order("title NULLS FIRST").reverse_order end - end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title nulls last").reverse_order + end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title NULLS FIRST, author_name").reverse_order + end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("author_name, title nulls last").reverse_order + end + end if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_default_reverse_order_on_table_without_primary_key assert_raises(ActiveRecord::IrreversibleOrderError) do @@ -480,21 +539,6 @@ class RelationTest < ActiveRecord::TestCase assert_nothing_raised { Topic.reorder([]) } end - def test_respond_to_delegates_to_arel - relation = Topic.all - fake_arel = Struct.new(:responds) { - def respond_to?(method, access = false) - responds << [method, access] - end - }.new [] - - relation.extend(Module.new { attr_accessor :arel }) - relation.arel = fake_arel - - relation.respond_to?(:matching_attributes) - assert_equal [:matching_attributes, false], fake_arel.responds.first - end - def test_respond_to_dynamic_finders relation = Topic.all @@ -558,6 +602,13 @@ class RelationTest < ActiveRecord::TestCase end end + def test_extracted_association + relation_authors = assert_queries(2) { Post.all.extract_associated(:author) } + root_authors = assert_queries(2) { Post.extract_associated(:author) } + assert_equal relation_authors, root_authors + assert_equal Post.all.collect(&:author), relation_authors + end + def test_find_with_included_associations assert_queries(2) do posts = Post.includes(:comments).order("posts.id") @@ -655,7 +706,7 @@ class RelationTest < ActiveRecord::TestCase end def test_to_sql_on_eager_join - expected = assert_sql { + expected = capture_sql { Post.eager_load(:last_comment).order("comments.id DESC").to_a }.first actual = Post.eager_load(:last_comment).order("comments.id DESC").to_sql @@ -934,12 +985,25 @@ class RelationTest < ActiveRecord::TestCase assert_queries(1) { assert_equal 11, posts.load.size } end + def test_size_with_eager_loading_and_custom_select_and_order + posts = Post.includes(:comments).order("comments.id").select(:type) + assert_queries(1) { assert_equal 11, posts.size } + assert_queries(1) { assert_equal 11, posts.load.size } + end + def test_size_with_eager_loading_and_custom_order_and_distinct posts = Post.includes(:comments).order("comments.id").distinct assert_queries(1) { assert_equal 11, posts.size } assert_queries(1) { assert_equal 11, posts.load.size } end + def test_size_with_eager_loading_and_manual_distinct_select_and_custom_order + accounts = Account.select("DISTINCT accounts.firm_id").order("accounts.firm_id") + + assert_queries(1) { assert_equal 5, accounts.size } + assert_queries(1) { assert_equal 5, accounts.load.size } + end + def test_count_explicit_columns Post.update_all(comments_count: nil) posts = Post.all @@ -1181,8 +1245,23 @@ class RelationTest < ActiveRecord::TestCase assert_equal "green", parrot.color end + def test_first_or_create_with_after_initialize + Bird.create!(color: "yellow", name: "canary") + parrot = assert_deprecated do + Bird.where(color: "green").first_or_create do |bird| + bird.name = "parrot" + bird.enable_count = true + end + end + assert_equal 0, parrot.total_count + end + def test_first_or_create_with_block - parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" } + Bird.create!(color: "yellow", name: "canary") + parrot = Bird.where(color: "green").first_or_create do |bird| + bird.name = "parrot" + assert_deprecated { assert_equal 0, Bird.count } + end assert_kind_of Bird, parrot assert_predicate parrot, :persisted? assert_equal "green", parrot.color @@ -1223,8 +1302,23 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordInvalid) { Bird.where(color: "green").first_or_create! } end + def test_first_or_create_bang_with_after_initialize + Bird.create!(color: "yellow", name: "canary") + parrot = assert_deprecated do + Bird.where(color: "green").first_or_create! do |bird| + bird.name = "parrot" + bird.enable_count = true + end + end + assert_equal 0, parrot.total_count + end + def test_first_or_create_bang_with_valid_block - parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" } + Bird.create!(color: "yellow", name: "canary") + parrot = Bird.where(color: "green").first_or_create! do |bird| + bird.name = "parrot" + assert_deprecated { assert_equal 0, Bird.count } + end assert_kind_of Bird, parrot assert_predicate parrot, :persisted? assert_equal "green", parrot.color @@ -1273,8 +1367,23 @@ class RelationTest < ActiveRecord::TestCase assert_equal "green", parrot.color end + def test_first_or_initialize_with_after_initialize + Bird.create!(color: "yellow", name: "canary") + parrot = assert_deprecated do + Bird.where(color: "green").first_or_initialize do |bird| + bird.name = "parrot" + bird.enable_count = true + end + end + assert_equal 0, parrot.total_count + end + def test_first_or_initialize_with_block - parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" } + Bird.create!(color: "yellow", name: "canary") + parrot = Bird.where(color: "green").first_or_initialize do |bird| + bird.name = "parrot" + assert_deprecated { assert_equal 0, Bird.count } + end assert_kind_of Bird, parrot assert_not_predicate parrot, :persisted? assert_predicate parrot, :valid? @@ -1315,6 +1424,13 @@ class RelationTest < ActiveRecord::TestCase assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat") end + def test_create_or_find_by_should_not_raise_due_to_validation_errors + assert_nothing_raised do + bird = Bird.create_or_find_by(color: "green") + assert_predicate bird, :invalid? + end + end + def test_create_or_find_by_with_non_unique_attributes Subscriber.create!(nick: "bob", name: "the builder") @@ -1334,6 +1450,38 @@ class RelationTest < ActiveRecord::TestCase end end + def test_create_or_find_by_with_bang + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + assert_equal subscriber, Subscriber.create_or_find_by!(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by!(nick: "cat") + end + + def test_create_or_find_by_with_bang_should_raise_due_to_validation_errors + assert_raises(ActiveRecord::RecordInvalid) { Bird.create_or_find_by!(color: "green") } + end + + def test_create_or_find_by_with_bang_with_non_unique_attributes + Subscriber.create!(nick: "bob", name: "the builder") + + assert_raises(ActiveRecord::RecordNotFound) do + Subscriber.create_or_find_by!(nick: "bob", name: "the cat") + end + end + + def test_create_or_find_by_with_bang_within_transaction + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + Subscriber.transaction do + assert_equal subscriber, Subscriber.create_or_find_by!(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by!(nick: "cat") + end + end + def test_find_or_initialize_by assert_nil Bird.find_by(name: "bob") @@ -1367,10 +1515,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal [posts(:welcome)], relation.to_a author_posts = relation.except(:order, :limit) - assert_equal Post.where(author_id: 1).to_a, author_posts.to_a + assert_equal Post.where(author_id: 1).sort_by(&:id), author_posts.sort_by(&:id) + assert_equal author_posts.sort_by(&:id), relation.scoping { Post.except(:order, :limit).sort_by(&:id) } all_posts = relation.except(:where, :order, :limit) - assert_equal Post.all, all_posts + assert_equal Post.all.sort_by(&:id), all_posts.sort_by(&:id) + assert_equal all_posts.sort_by(&:id), relation.scoping { Post.except(:where, :order, :limit).sort_by(&:id) } end def test_only @@ -1378,10 +1528,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal [posts(:welcome)], relation.to_a author_posts = relation.only(:where) - assert_equal Post.where(author_id: 1).to_a, author_posts.to_a + assert_equal Post.where(author_id: 1).sort_by(&:id), author_posts.sort_by(&:id) + assert_equal author_posts.sort_by(&:id), relation.scoping { Post.only(:where).sort_by(&:id) } - all_posts = relation.only(:limit) - assert_equal Post.limit(1).to_a, all_posts.to_a + all_posts = relation.only(:order) + assert_equal Post.order("id ASC").to_a, all_posts.to_a + assert_equal all_posts.to_a, relation.scoping { Post.only(:order).to_a } end def test_anonymous_extension @@ -1527,7 +1679,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body") assert_equal ["comments"], scope.references_values - scope = Post.order(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")) + scope = Post.order("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") if current_adapter?(:OracleAdapter) assert_equal ["COMMENTS"], scope.references_values else @@ -1544,7 +1696,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body asc") assert_equal ["comments"], scope.references_values - scope = Post.order(Arel.sql("foo(comments.body)")) + scope = Post.order("foo(comments.body)") assert_equal [], scope.references_values end @@ -1552,7 +1704,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body") assert_equal %w(comments), scope.references_values - scope = Post.reorder(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")) + scope = Post.reorder("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") if current_adapter?(:OracleAdapter) assert_equal ["COMMENTS"], scope.references_values else @@ -1569,7 +1721,7 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body asc") assert_equal %w(comments), scope.references_values - scope = Post.reorder(Arel.sql("foo(comments.body)")) + scope = Post.reorder("foo(comments.body)") assert_equal [], scope.references_values end @@ -1607,6 +1759,24 @@ class RelationTest < ActiveRecord::TestCase assert_predicate topics, :loaded? end + def test_delete_by + david = authors(:david) + + assert_difference("Post.count", -3) { david.posts.delete_by(body: "hello") } + + deleted = Author.delete_by(id: david.id) + assert_equal 1, deleted + end + + def test_destroy_by + david = authors(:david) + + assert_difference("Post.count", -3) { david.posts.destroy_by(body: "hello") } + + destroyed = Author.destroy_by(id: david.id) + assert_equal [david], destroyed + end + test "find_by with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by(author_id: 2) end @@ -1776,6 +1946,19 @@ class RelationTest < ActiveRecord::TestCase assert_equal [1, 1, 1], posts.map(&:author_address_id) end + test "joins with select custom attribute" do + contract = Company.create!(name: "test").contracts.create! + company = Company.joins(:contracts).select(:id, :metadata).find(contract.company_id) + assert_equal contract.metadata, company.metadata + end + + test "joins with order by custom attribute" do + companies = Company.create!([{ name: "test1" }, { name: "test2" }]) + companies.each { |company| company.contracts.create! } + assert_equal companies, Company.joins(:contracts).order(:metadata, :count) + assert_equal companies.reverse, Company.joins(:contracts).order(metadata: :desc, count: :desc) + end + test "delegations do not leak to other classes" do Topic.all.by_lifo assert Topic.all.class.method_defined?(:by_lifo) @@ -1794,6 +1977,30 @@ class RelationTest < ActiveRecord::TestCase assert_equal p2.first.comments, comments end + def test_unscope_with_merge + p0 = Post.where(author_id: 0) + p1 = Post.where(author_id: 1, comments_count: 1) + + assert_equal [posts(:authorless)], p0 + assert_equal [posts(:thinking)], p1 + + comments = Comment.merge(p0).unscope(where: :author_id).where(post: p1) + + assert_not_equal p0.first.comments, comments + assert_equal p1.first.comments, comments + end + + def test_unscope_with_unknown_column + comment = comments(:greetings) + comment.update!(comments: 1) + + comments = Comment.where(comments: 1).unscope(where: :unknown_column) + assert_equal [comment], comments + + comments = Comment.where(comments: 1).unscope(where: { comments: :unknown_column }) + assert_equal [comment], comments + end + def test_unscope_specific_where_value posts = Post.where(title: "Welcome to the weblog", body: "Such a lovely day") diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 778cf86ac3..6c884b4f45 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -148,6 +148,19 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "foo in (#{quoted_nil})", bind("foo in (?)", []) end + def test_bind_range + quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) + assert_equal "0", bind("?", 0..0) + assert_equal "1,2,3", bind("?", 1..3) + assert_equal quoted_abc, bind("?", "a"..."d") + end + + def test_bind_empty_range + quoted_nil = ActiveRecord::Base.connection.quote(nil) + assert_equal quoted_nil, bind("?", 0...0) + assert_equal quoted_nil, bind("?", "a"..."a") + end + def test_bind_empty_string quoted_empty = ActiveRecord::Base.connection.quote("") assert_equal quoted_empty, bind("?", "") @@ -168,12 +181,6 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call end - def test_deprecated_expand_hash_conditions_for_aggregates - assert_deprecated do - assert_equal({ "balance" => 50 }, Customer.send(:expand_hash_conditions_for_aggregates, balance: Money.new(50))) - end - end - private def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index dda3efa47c..bb7184c5fc 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -33,6 +33,7 @@ class SchemaDumperTest < ActiveRecord::TestCase schema_info = ActiveRecord::Base.connection.dump_schema_information assert_match(/20100201010101.*20100301010101/m, schema_info) + assert_includes schema_info, "20100101010101" ensure ActiveRecord::SchemaMigration.delete_all end @@ -245,25 +246,31 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:Mysql2Adapter) def test_schema_dump_includes_length_for_mysql_binary_fields - output = standard_dump + output = dump_table_schema "binary_fields" assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output end def test_schema_dump_includes_length_for_mysql_blob_and_text_fields - output = standard_dump - assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output + output = dump_table_schema "binary_fields" + assert_match %r{t\.binary\s+"tiny_blob",\s+size: :tiny$}, output assert_match %r{t\.binary\s+"normal_blob"$}, output - assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output - assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output - assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output + assert_match %r{t\.binary\s+"medium_blob",\s+size: :medium$}, output + assert_match %r{t\.binary\s+"long_blob",\s+size: :long$}, output + assert_match %r{t\.text\s+"tiny_text",\s+size: :tiny$}, output assert_match %r{t\.text\s+"normal_text"$}, output - assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output - assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output + assert_match %r{t\.text\s+"medium_text",\s+size: :medium$}, output + assert_match %r{t\.text\s+"long_text",\s+size: :long$}, output + assert_match %r{t\.binary\s+"tiny_blob_2",\s+size: :tiny$}, output + assert_match %r{t\.binary\s+"medium_blob_2",\s+size: :medium$}, output + assert_match %r{t\.binary\s+"long_blob_2",\s+size: :long$}, output + assert_match %r{t\.text\s+"tiny_text_2",\s+size: :tiny$}, output + assert_match %r{t\.text\s+"medium_text_2",\s+size: :medium$}, output + assert_match %r{t\.text\s+"long_text_2",\s+size: :long$}, output end def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields - output = standard_dump + output = dump_table_schema "booleans" assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output end diff --git a/activerecord/test/cases/schema_loading_test.rb b/activerecord/test/cases/schema_loading_test.rb index f539156466..5da2d9e08f 100644 --- a/activerecord/test/cases/schema_loading_test.rb +++ b/activerecord/test/cases/schema_loading_test.rb @@ -43,7 +43,6 @@ class SchemaLoadingTest < ActiveRecord::TestCase end private - def define_model Class.new(ActiveRecord::Base) do include SchemaLoadCounter diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 6281712df6..e7bdab58c6 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -408,18 +408,18 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_joins_not_affected_by_scope_other_than_default_or_unscoped - without_scope_on_post = Comment.joins(:post).to_a + without_scope_on_post = Comment.joins(:post).sort_by(&:id) with_scope_on_post = nil Post.where(id: [1, 5, 6]).scoping do - with_scope_on_post = Comment.joins(:post).to_a + with_scope_on_post = Comment.joins(:post).sort_by(&:id) end - assert_equal with_scope_on_post, without_scope_on_post + assert_equal without_scope_on_post, with_scope_on_post end def test_unscoped_with_joins_should_not_have_default_scope - assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a }, - Comment.joins(:post).to_a + assert_equal Comment.joins(:post).sort_by(&:id), + SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).sort_by(&:id) } end def test_sti_association_with_unscoped_not_affected_by_default_scope diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index f707951a16..3488442cab 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -50,7 +50,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_calling_merge_at_first_in_scope Topic.class_eval do - scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.replied) } + scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.unscoped.replied) } end assert_equal Topic.calling_merge_at_first_in_scope.to_a, Topic.replied.to_a end @@ -303,13 +303,6 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal "lifo", topic.author_name end - def test_deprecated_delegating_private_method - assert_deprecated do - scope = Topic.all.by_private_lifo - assert_not scope.instance_variable_get(:@delegate_to_klass) - end - end - def test_reserved_scope_names klass = Class.new(ActiveRecord::Base) do self.table_name = "topics" @@ -454,6 +447,17 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq end + def test_class_method_in_scope + assert_deprecated do + assert_equal [topics(:second)], topics(:first).approved_replies.ordered + end + end + + def test_nested_scoping + expected = Reply.approved + assert_equal expected.to_a, Topic.rejected.nested_scoping(expected) + end + def test_scopes_batch_finders assert_equal 4, Topic.approved.count @@ -598,4 +602,14 @@ class NamedScopingTest < ActiveRecord::TestCase Topic.create! assert_predicate Topic, :one? end + + def test_scope_with_annotation + Topic.class_eval do + scope :including_annotate_in_scope, Proc.new { annotate("from-scope") } + end + + assert_sql(%r{/\* from-scope \*/}) do + assert Topic.including_annotate_in_scope.to_a, Topic.all.to_a + end + end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index b4f4379e5e..50b514d464 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -130,6 +130,44 @@ class RelationScopingTest < ActiveRecord::TestCase end end + def test_scoped_find_with_annotation + Developer.annotate("scoped").scoping do + developer = nil + assert_sql(%r{/\* scoped \*/}) do + developer = Developer.where("name = 'David'").first + end + assert_equal "David", developer.name + end + end + + def test_find_with_annotation_unscoped + Developer.annotate("scoped").unscoped do + developer = nil + log = capture_sql do + developer = Developer.where("name = 'David'").first + end + + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\* scoped \*/}) }, :empty? + + assert_equal "David", developer.name + end + end + + def test_find_with_annotation_unscope + developer = nil + log = capture_sql do + developer = Developer.annotate("unscope"). + where("name = 'David'"). + unscope(:annotate).first + end + + assert_not_predicate log, :empty? + assert_predicate log.select { |query| query.match?(%r{/\* unscope \*/}) }, :empty? + + assert_equal "David", developer.name + end + def test_scoped_find_include # with the include, will retrieve only developers for the given project scoped_developers = Developer.includes(:projects).scoping do @@ -254,11 +292,16 @@ class RelationScopingTest < ActiveRecord::TestCase end end - def test_scoping_works_in_the_scope_block + def test_scoping_with_klass_method_works_in_the_scope_block expected = SpecialPostWithDefaultScope.unscoped.to_a assert_equal expected, SpecialPostWithDefaultScope.unscoped_all end + def test_scoping_with_query_method_works_in_the_scope_block + expected = SpecialPostWithDefaultScope.unscoped.where(author_id: 0).to_a + assert_equal expected, SpecialPostWithDefaultScope.authorless + end + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do Post.first(10) @@ -368,7 +411,19 @@ class HasManyScopingTest < ActiveRecord::TestCase def test_nested_scope_finder Comment.where("1=0").scoping do - assert_equal 0, @welcome.comments.count + assert_equal 2, @welcome.comments.count + assert_equal "a comment...", @welcome.comments.what_are_you + end + + Comment.where("1=1").scoping do + assert_equal 2, @welcome.comments.count + assert_equal "a comment...", @welcome.comments.what_are_you + end + end + + def test_none_scoping + Comment.none.scoping do + assert_equal 2, @welcome.comments.count assert_equal "a comment...", @welcome.comments.what_are_you end @@ -409,7 +464,19 @@ class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase def test_nested_scope_finder Category.where("1=0").scoping do - assert_equal 0, @welcome.categories.count + assert_equal 2, @welcome.categories.count + assert_equal "a category...", @welcome.categories.what_are_you + end + + Category.where("1=1").scoping do + assert_equal 2, @welcome.categories.count + assert_equal "a category...", @welcome.categories.what_are_you + end + end + + def test_none_scoping + Category.none.scoping do + assert_equal 2, @welcome.categories.count assert_equal "a category...", @welcome.categories.what_are_you end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 1192b30b14..ecf81b2042 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -1,18 +1,23 @@ # frozen_string_literal: true require "cases/helper" -require "models/topic" -require "models/reply" require "models/person" require "models/traffic_light" require "models/post" -require "bcrypt" class SerializedAttributeTest < ActiveRecord::TestCase fixtures :topics, :posts MyObject = Struct.new :attribute1, :attribute2 + class Topic < ActiveRecord::Base + serialize :content + end + + class ImportantTopic < Topic + serialize :important, Hash + end + teardown do Topic.serialize("content") end @@ -49,10 +54,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase def test_serialized_attributes_from_database_on_subclass Topic.serialize :content, Hash - t = Reply.new(content: { foo: :bar }) + t = ImportantTopic.new(content: { foo: :bar }) assert_equal({ foo: :bar }, t.content) t.save! - t = Reply.last + t = ImportantTopic.last assert_equal({ foo: :bar }, t.content) end @@ -367,9 +372,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialized_attribute_works_under_concurrent_initial_access - model = Topic.dup + model = Class.new(Topic) - topic = model.last + topic = model.create! topic.update group: "1" model.serialize :group, JSON diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index e3c12f68fd..6a6d73dc38 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/book" require "models/liquid" require "models/molecule" +require "models/numeric_data" require "models/electron" module ActiveRecord @@ -74,6 +75,11 @@ module ActiveRecord assert_equal "salty", liquids[0].name end + def test_statement_cache_with_strictly_cast_attribute + row = NumericData.create(temperature: 1.5) + assert_equal row, NumericData.find_by(temperature: 1.5) + end + def test_statement_cache_values_differ cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book") diff --git a/activerecord/test/cases/statement_invalid_test.rb b/activerecord/test/cases/statement_invalid_test.rb new file mode 100644 index 0000000000..16ea69c1bd --- /dev/null +++ b/activerecord/test/cases/statement_invalid_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/book" + +module ActiveRecord + class StatementInvalidTest < ActiveRecord::TestCase + fixtures :books + + class MockDatabaseError < StandardError + def result + 0 + end + + def error_number + 0 + end + end + + test "message contains no sql" do + sql = Book.where(author_id: 96, cover: "hard").to_sql + error = assert_raises(ActiveRecord::StatementInvalid) do + Book.connection.send(:log, sql, Book.name) do + raise MockDatabaseError + end + end + assert_not error.message.include?("SELECT") + end + + test "statement and binds are set on select" do + sql = Book.where(author_id: 96, cover: "hard").to_sql + binds = [Minitest::Mock.new, Minitest::Mock.new] + error = assert_raises(ActiveRecord::StatementInvalid) do + Book.connection.send(:log, sql, Book.name, binds) do + raise MockDatabaseError + end + end + assert_equal error.sql, sql + assert_equal error.binds, binds + end + end +end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 4457cfbd37..91c0e959f4 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -79,6 +79,74 @@ class StoreTest < ActiveRecord::TestCase assert_not_predicate @john, :settings_changed? end + test "updating the store will mark accessor as changed" do + @john.color = "red" + assert @john.color_changed? + end + + test "new record and no accessors changes" do + user = Admin::User.new + assert_not user.color_changed? + assert_nil user.color_was + assert_nil user.color_change + + user.color = "red" + assert user.color_changed? + assert_nil user.color_was + assert_equal "red", user.color_change[1] + end + + test "updating the store won't mark accessor as changed if the whole store was updated" do + @john.settings = { color: @john.color, some: "thing" } + assert @john.settings_changed? + assert_not @john.color_changed? + end + + test "updating the store populates the accessor changed array correctly" do + @john.color = "red" + assert_equal "black", @john.color_was + assert_equal "black", @john.color_change[0] + assert_equal "red", @john.color_change[1] + end + + test "updating the store won't mark accessor as changed if the value isn't changed" do + @john.color = @john.color + assert_not @john.color_changed? + end + + test "nullifying the store mark accessor as changed" do + color = @john.color + @john.settings = nil + assert @john.color_changed? + assert_equal color, @john.color_was + assert_equal [color, nil], @john.color_change + end + + test "dirty methods for suffixed accessors" do + @john.configs[:two_factor_auth] = true + assert @john.two_factor_auth_configs_changed? + assert_nil @john.two_factor_auth_configs_was + assert_equal [nil, true], @john.two_factor_auth_configs_change + end + + test "dirty methods for prefixed accessors" do + @john.spouse[:name] = "Lena" + assert @john.partner_name_changed? + assert_equal "Dallas", @john.partner_name_was + assert_equal ["Dallas", "Lena"], @john.partner_name_change + end + + test "saved changes tracking for accessors" do + @john.spouse[:name] = "Lena" + assert @john.partner_name_changed? + + @john.save! + assert_not @john.partner_name_change + assert @john.saved_change_to_partner_name? + assert_equal ["Dallas", "Lena"], @john.saved_change_to_partner_name + assert_equal "Dallas", @john.partner_name_before_last_save + end + test "object initialization with not nullable column" do assert_equal true, @john.remember_login end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 3fd1813d64..6b6861465b 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "active_record/tasks/database_tasks" +require "models/author" module ActiveRecord module DatabaseTasksSetupper @@ -49,6 +50,8 @@ module ActiveRecord protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment + InternalMetadata[:environment] = current_env + assert_called_on_instance_of( ActiveRecord::MigrationContext, :current_version, @@ -72,6 +75,9 @@ module ActiveRecord def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment + + InternalMetadata[:environment] = current_env + assert_called_on_instance_of( ActiveRecord::MigrationContext, :current_version, @@ -754,7 +760,7 @@ module ActiveRecord end class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase - def test_migrate_set_and_unset_verbose_and_version_env_vars + def test_can_migrate_from_pending_migration_error_action_dispatch verbose, version = ENV["VERBOSE"], ENV["VERSION"] ENV["VERSION"] = "2" ENV["VERBOSE"] = "false" @@ -766,7 +772,9 @@ module ActiveRecord ENV.delete("VERBOSE") # re-run up migration - assert_includes capture_migration_output, "migrating" + assert_includes(capture(:stdout) do + ActiveSupport::ActionableError.dispatch ActiveRecord::PendingMigrationError, "Run pending migrations" + end, "migrating") ensure ENV["VERBOSE"], ENV["VERSION"] = verbose, version end @@ -827,7 +835,6 @@ module ActiveRecord end private - def capture_migration_status capture(:stdout) do ActiveRecord::Tasks::DatabaseTasks.migrate_status @@ -944,6 +951,176 @@ module ActiveRecord end end + unless in_memory_db? + class DatabaseTasksTruncateAllTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :authors, :author_addresses + + def setup + SchemaMigration.create_table + SchemaMigration.create!(version: SchemaMigration.table_name) + InternalMetadata.create_table + InternalMetadata.create!(key: InternalMetadata.table_name) + end + + def teardown + SchemaMigration.delete_all + InternalMetadata.delete_all + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + + def test_truncate_tables + assert_operator SchemaMigration.count, :>, 0 + assert_operator InternalMetadata.count, :>, 0 + assert_operator Author.count, :>, 0 + assert_operator AuthorAddress.count, :>, 0 + + old_configurations = ActiveRecord::Base.configurations + configurations = { development: ActiveRecord::Base.configurations["arunit"] } + ActiveRecord::Base.configurations = configurations + + ActiveRecord::Tasks::DatabaseTasks.stub(:root, nil) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("development") + ) + end + + assert_operator SchemaMigration.count, :>, 0 + assert_operator InternalMetadata.count, :>, 0 + assert_equal 0, Author.count + assert_equal 0, AuthorAddress.count + ensure + ActiveRecord::Base.configurations = old_configurations + end + end + + class DatabaseTasksTruncateAllWithPrefixTest < DatabaseTasksTruncateAllTest + setup do + ActiveRecord::Base.table_name_prefix = "p_" + + SchemaMigration.reset_table_name + InternalMetadata.reset_table_name + end + + teardown do + ActiveRecord::Base.table_name_prefix = nil + + SchemaMigration.reset_table_name + InternalMetadata.reset_table_name + end + end + + class DatabaseTasksTruncateAllWithSuffixTest < DatabaseTasksTruncateAllTest + setup do + ActiveRecord::Base.table_name_suffix = "_s" + + SchemaMigration.reset_table_name + InternalMetadata.reset_table_name + end + + teardown do + ActiveRecord::Base.table_name_suffix = nil + + SchemaMigration.reset_table_name + InternalMetadata.reset_table_name + end + end + end + + class DatabaseTasksTruncateAllWithMultipleDatabasesTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } + } + end + + def test_truncate_all_databases_for_environment + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["database" => "test-db"], + ["database" => "secondary-test-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("test") + ) + end + end + end + + def test_truncate_all_databases_with_url_for_environment + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("production") + ) + end + end + end + + def test_truncate_all_development_databases_when_env_is_not_specified + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("development") + ) + end + end + end + + def test_truncate_all_development_databases_when_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + + with_stubbed_configurations do + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :truncate_tables, + [ + ["database" => "dev-db"], + ["database" => "secondary-dev-db"] + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.truncate_all( + ActiveSupport::StringInquirer.new("development") + ) + end + end + ensure + ENV["RAILS_ENV"] = old_env + end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end + end + class DatabaseTasksCharsetTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 552e623fd4..258132835f 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -100,7 +100,6 @@ if current_adapter?(:Mysql2Adapter) end private - def with_stubbed_connection_establish_connection ActiveRecord::Base.stub(:establish_connection, nil) do ActiveRecord::Base.stub(:connection, @connection) do @@ -180,7 +179,6 @@ if current_adapter?(:Mysql2Adapter) end private - def with_stubbed_connection_establish_connection ActiveRecord::Base.stub(:establish_connection, nil) do ActiveRecord::Base.stub(:connection, @connection) do @@ -233,7 +231,6 @@ if current_adapter?(:Mysql2Adapter) end private - def with_stubbed_connection_establish_connection ActiveRecord::Base.stub(:establish_connection, nil) do ActiveRecord::Base.stub(:connection, @connection) do diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 065ba7734c..f9df650687 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -139,7 +139,6 @@ if current_adapter?(:PostgreSQLAdapter) end private - def with_stubbed_connection_establish_connection ActiveRecord::Base.stub(:connection, @connection) do ActiveRecord::Base.stub(:establish_connection, nil) do @@ -201,7 +200,6 @@ if current_adapter?(:PostgreSQLAdapter) end private - def with_stubbed_connection_establish_connection ActiveRecord::Base.stub(:connection, @connection) do ActiveRecord::Base.stub(:establish_connection, nil) do @@ -301,7 +299,6 @@ if current_adapter?(:PostgreSQLAdapter) end private - def with_stubbed_connection ActiveRecord::Base.stub(:connection, @connection) do yield diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 40947767f3..1b8bad32a4 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -34,7 +34,7 @@ module ActiveRecord ActiveRecord::Base.connection.materialize_transactions SQLCounter.clear_log yield - SQLCounter.log_all.dup + SQLCounter.log.dup end def assert_sql(*patterns_to_match) @@ -79,10 +79,6 @@ module ActiveRecord model.reset_column_information model.column_names.include?(column_name.to_s) end - - def frozen_error_class - Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError - end end class PostgreSQLTestCase < TestCase @@ -111,32 +107,12 @@ module ActiveRecord clear_log - self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/] - - # FIXME: this needs to be refactored so specific database can add their own - # ignored SQL, or better yet, use a different notification for the queries - # instead examining the SQL content. - oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im] - mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im] - postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im] - sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im] - - [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| - ignored_sql.concat db_ignored_sql - end - - attr_reader :ignore - - def initialize(ignore = Regexp.union(self.class.ignored_sql)) - @ignore = ignore - end - def call(name, start, finish, message_id, values) return if values[:cached] sql = values[:sql] self.class.log_all << sql - self.class.log << sql unless ignore.match?(sql) + self.class.log << sql unless ["SCHEMA", "TRANSACTION"].include? values[:name] end end diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 086500de38..1abd857216 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -45,6 +45,26 @@ if subsecond_precision_supported? assert_equal 123456000, foo.finish.nsec end + unless current_adapter?(:Mysql2Adapter) + def test_no_time_precision_isnt_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :start, :time + @connection.add_column :foos, :finish, :time, precision: 6 + + time = ::Time.now.change(nsec: 123) + foo = Foo.new(start: time, finish: time) + + assert_equal 123, foo.start.nsec + assert_equal 0, foo.finish.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.start.nsec + assert_equal 0, foo.finish.nsec + end + end + def test_passing_precision_to_time_does_not_set_limit @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 3 @@ -55,7 +75,7 @@ if subsecond_precision_supported? end def test_invalid_time_precision_raises_error - assert_raises ActiveRecord::ActiveRecordError do + assert_raises ArgumentError do @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 7 t.time :finish, precision: 7 diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 75ecd6fc40..232e018e03 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -40,17 +40,25 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal @previously_updated_at, @developer.updated_at assert_equal previous_salary + 10000, @developer.salary - assert @developer.salary_changed?, "developer salary should have changed" - assert @developer.changed?, "developer should be marked as changed" + assert_predicate @developer, :salary_changed?, "developer salary should have changed" + assert_predicate @developer, :changed?, "developer should be marked as changed" + assert_equal ["salary"], @developer.changed + assert_predicate @developer, :saved_changes? + assert_equal ["updated_at", "updated_on"], @developer.saved_changes.keys.sort + @developer.reload assert_equal previous_salary, @developer.salary end def test_touching_a_record_with_default_scope_that_excludes_it_updates_its_timestamp developer = @developer.becomes(DeveloperCalledJamis) - developer.touch + assert_not_equal @previously_updated_at, developer.updated_at + assert_not_predicate developer, :changed? + assert_predicate developer, :saved_changes? + assert_equal ["updated_at", "updated_on"], developer.saved_changes.keys.sort + developer.reload assert_not_equal @previously_updated_at, developer.updated_at end diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index cd3d5ed7d1..f1a9cf2d05 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -10,7 +10,7 @@ require "models/tree" class TouchLaterTest < ActiveRecord::TestCase fixtures :nodes, :trees - def test_touch_laster_raise_if_non_persisted + def test_touch_later_raise_if_non_persisted invoice = Invoice.new Invoice.transaction do assert_not_predicate invoice, :persisted? diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index aa6b7915a2..19b89ab08c 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -36,8 +36,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" + before_destroy { self.class.find(id).touch if persisted? } + before_commit { |record| record.do_before_commit(nil) } after_commit { |record| record.do_after_commit(nil) } + after_save_commit { |record| record.do_after_commit(:save) } after_create_commit { |record| record.do_after_commit(:create) } after_update_commit { |record| record.do_after_commit(:update) } after_destroy_commit { |record| record.do_after_commit(:destroy) } @@ -110,6 +113,43 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:after_commit], @first.history end + def test_dont_call_any_callbacks_after_transaction_commits_for_invalid_record + @first.after_commit_block { |r| r.history << :after_commit } + @first.after_rollback_block { |r| r.history << :after_rollback } + + def @first.valid?(*) + false + end + + assert_not @first.save + assert_equal [], @first.history + end + + def test_dont_call_any_callbacks_after_explicit_transaction_commits_for_invalid_record + @first.after_commit_block { |r| r.history << :after_commit } + @first.after_rollback_block { |r| r.history << :after_rollback } + + def @first.valid?(*) + false + end + + @first.transaction do + assert_not @first.save + end + assert_equal [], @first.history + end + + def test_only_call_after_commit_on_save_after_transaction_commits_for_saving_record + record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today) + record.after_commit_block(:save) { |r| r.history << :after_save } + + record.save! + assert_equal [:after_save], record.history + + record.update!(title: "Another topic") + assert_equal [:after_save, :after_save], record.history + end + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record add_transaction_execution_blocks @first @@ -420,7 +460,6 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end private - def add_transaction_execution_blocks(record) record.after_commit_block(:create) { |r| r.history << :commit_on_create } record.after_commit_block(:update) { |r| r.history << :commit_on_update } @@ -511,6 +550,8 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase end class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase + self.use_transactional_tests = false + class TopicWithHistory < ActiveRecord::Base self.table_name = :topics @@ -524,11 +565,22 @@ class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase end class TopicWithCallbacksOnDestroy < TopicWithHistory - after_commit(on: :destroy) { |record| record.class.history << :destroy } + after_commit(on: :destroy) { |record| record.class.history << :commit_on_destroy } + after_rollback(on: :destroy) { |record| record.class.history << :rollback_on_destroy } + + before_destroy :before_destroy_for_transaction + + private + def before_destroy_for_transaction; end end class TopicWithCallbacksOnUpdate < TopicWithHistory - after_commit(on: :update) { |record| record.class.history << :update } + after_commit(on: :update) { |record| record.class.history << :commit_on_update } + + before_save :before_save_for_transaction + + private + def before_save_for_transaction; end end def test_trigger_once_on_multiple_deletions @@ -536,10 +588,39 @@ class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase topic = TopicWithCallbacksOnDestroy.new topic.save topic_clone = TopicWithCallbacksOnDestroy.find(topic.id) + + topic.define_singleton_method(:before_destroy_for_transaction) do + topic_clone.destroy + end + topic.destroy - topic_clone.destroy - assert_equal [:destroy], TopicWithCallbacksOnDestroy.history + assert_equal [:commit_on_destroy], TopicWithCallbacksOnDestroy.history + end + + def test_rollback_on_multiple_deletions + TopicWithCallbacksOnDestroy.clear_history + topic = TopicWithCallbacksOnDestroy.new + topic.save + topic_clone = TopicWithCallbacksOnDestroy.find(topic.id) + + topic.define_singleton_method(:before_destroy_for_transaction) do + topic_clone.update!(author_name: "Test Author Clone") + topic_clone.destroy + end + + TopicWithCallbacksOnDestroy.transaction do + topic.update!(author_name: "Test Author") + topic.destroy + raise ActiveRecord::Rollback + end + + assert_not_predicate topic, :destroyed? + assert_not_predicate topic_clone, :destroyed? + assert_equal [nil, "Test Author"], topic.author_name_change_to_be_saved + assert_equal [nil, "Test Author Clone"], topic_clone.author_name_change_to_be_saved + + assert_equal [:rollback_on_destroy], TopicWithCallbacksOnDestroy.history end def test_trigger_on_update_where_row_was_deleted @@ -547,7 +628,11 @@ class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase topic = TopicWithCallbacksOnUpdate.new topic.save topic_clone = TopicWithCallbacksOnUpdate.find(topic.id) - topic.destroy + + topic_clone.define_singleton_method(:before_save_for_transaction) do + topic.destroy + end + topic_clone.author_name = "Test Author" topic_clone.save @@ -586,7 +671,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase @topic.content = "foo" @topic.save! end - @topic.class.connection.add_transaction_record(@topic) + @topic.send(:add_to_transaction) end assert_equal [:before_commit, :after_commit], @topic.history end @@ -596,7 +681,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase @topic.transaction(requires_new: true) do @topic.content = "foo" @topic.save! - @topic.class.connection.add_transaction_record(@topic) + @topic.send(:add_to_transaction) end end assert_equal [:before_commit, :after_commit], @topic.history @@ -617,7 +702,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase @topic.content = "foo" @topic.save! end - @topic.class.connection.add_transaction_record(@topic) + @topic.send(:add_to_transaction) raise ActiveRecord::Rollback end assert_equal [:rollback], @topic.history diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 50740054f7..b5c1cac3d9 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -18,6 +18,65 @@ class TransactionTest < ActiveRecord::TestCase @first, @second = Topic.find(1, 2).sort_by(&:id) end + def test_rollback_dirty_changes + topic = topics(:fifth) + + ActiveRecord::Base.transaction do + topic.update(title: "Ruby on Rails") + raise ActiveRecord::Rollback + end + + title_change = ["The Fifth Topic of the day", "Ruby on Rails"] + assert_equal title_change, topic.changes["title"] + end + + def test_rollback_dirty_changes_multiple_saves + topic = topics(:fifth) + + ActiveRecord::Base.transaction do + topic.update(title: "Ruby on Rails") + topic.update(title: "Another Title") + raise ActiveRecord::Rollback + end + + title_change = ["The Fifth Topic of the day", "Another Title"] + assert_equal title_change, topic.changes["title"] + end + + def test_rollback_dirty_changes_then_retry_save + topic = topics(:fifth) + + ActiveRecord::Base.transaction do + topic.update(title: "Ruby on Rails") + raise ActiveRecord::Rollback + end + + title_change = ["The Fifth Topic of the day", "Ruby on Rails"] + assert_equal title_change, topic.changes["title"] + + assert topic.save + + assert_equal title_change, topic.saved_changes["title"] + assert_equal topic.title, topic.reload.title + end + + def test_rollback_dirty_changes_then_retry_save_on_new_record + topic = Topic.new(title: "Ruby on Rails") + + ActiveRecord::Base.transaction do + topic.save + raise ActiveRecord::Rollback + end + + title_change = [nil, "Ruby on Rails"] + assert_equal title_change, topic.changes["title"] + + assert topic.save + + assert_equal title_change, topic.saved_changes["title"] + assert_equal topic.title, topic.reload.title + end + def test_persisted_in_a_model_with_custom_primary_key_after_failed_save movie = Movie.create assert_not_predicate movie, :persisted? @@ -26,28 +85,31 @@ class TransactionTest < ActiveRecord::TestCase def test_raise_after_destroy assert_not_predicate @first, :frozen? - assert_raises(RuntimeError) { - Topic.transaction do - @first.destroy - assert_predicate @first, :frozen? - raise + assert_not_called(@first, :rolledback!) do + assert_raises(RuntimeError) do + Topic.transaction do + @first.destroy + assert_predicate @first, :frozen? + raise + end end - } + end - assert @first.reload assert_not_predicate @first, :frozen? end def test_successful - Topic.transaction do - @first.approved = true - @second.approved = false - @first.save - @second.save + assert_not_called(@first, :committed!) do + Topic.transaction do + @first.approved = true + @second.approved = false + @first.save + @second.save + end end - assert Topic.find(1).approved?, "First should have been approved" - assert_not Topic.find(2).approved?, "Second should have been unapproved" + assert_predicate Topic.find(1), :approved?, "First should have been approved" + assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved" end def transaction_with_return @@ -62,7 +124,7 @@ class TransactionTest < ActiveRecord::TestCase def test_add_to_null_transaction topic = Topic.new - topic.add_to_transaction + topic.send(:add_to_transaction) end def test_successful_with_return @@ -76,11 +138,13 @@ class TransactionTest < ActiveRecord::TestCase end end - transaction_with_return + assert_not_called(@first, :committed!) do + transaction_with_return + end assert committed - assert Topic.find(1).approved?, "First should have been approved" - assert_not Topic.find(2).approved?, "Second should have been unapproved" + assert_predicate Topic.find(1), :approved?, "First should have been approved" + assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved" ensure Topic.connection.class_eval do remove_method :commit_db_transaction @@ -99,9 +163,11 @@ class TransactionTest < ActiveRecord::TestCase end end - Topic.transaction do - @first.approved = true - @first.save! + assert_not_called(@first, :committed!) do + Topic.transaction do + @first.approved = true + @first.save! + end end assert_equal 0, num @@ -113,19 +179,21 @@ class TransactionTest < ActiveRecord::TestCase end def test_successful_with_instance_method - @first.transaction do - @first.approved = true - @second.approved = false - @first.save - @second.save + assert_not_called(@first, :committed!) do + @first.transaction do + @first.approved = true + @second.approved = false + @first.save + @second.save + end end - assert Topic.find(1).approved?, "First should have been approved" - assert_not Topic.find(2).approved?, "Second should have been unapproved" + assert_predicate Topic.find(1), :approved?, "First should have been approved" + assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved" end def test_failing_on_exception - begin + assert_not_called(@first, :rolledback!) do Topic.transaction do @first.approved = true @second.approved = false @@ -137,11 +205,11 @@ class TransactionTest < ActiveRecord::TestCase # caught it end - assert @first.approved?, "First should still be changed in the objects" - assert_not @second.approved?, "Second should still be changed in the objects" + assert_predicate @first, :approved?, "First should still be changed in the objects" + assert_not_predicate @second, :approved?, "Second should still be changed in the objects" - assert_not Topic.find(1).approved?, "First shouldn't have been approved" - assert Topic.find(2).approved?, "Second should still be approved" + assert_not_predicate Topic.find(1), :approved?, "First shouldn't have been approved" + assert_predicate Topic.find(2), :approved?, "Second should still be approved" end def test_raising_exception_in_callback_rollbacks_in_save @@ -150,8 +218,10 @@ class TransactionTest < ActiveRecord::TestCase end @first.approved = true - e = assert_raises(RuntimeError) { @first.save } - assert_equal "Make the transaction rollback", e.message + assert_not_called(@first, :rolledback!) do + e = assert_raises(RuntimeError) { @first.save } + assert_equal "Make the transaction rollback", e.message + end assert_not_predicate Topic.find(1), :approved? end @@ -159,13 +229,15 @@ class TransactionTest < ActiveRecord::TestCase def @first.before_save_for_transaction raise ActiveRecord::Rollback end - assert_not @first.approved + assert_not_predicate @first, :approved? - Topic.transaction do - @first.approved = true - @first.save! + assert_not_called(@first, :rolledback!) do + Topic.transaction do + @first.approved = true + @first.save! + end end - assert_not Topic.find(@first.id).approved?, "Should not commit the approved flag" + assert_not_predicate Topic.find(@first.id), :approved?, "Should not commit the approved flag" end def test_raising_exception_in_nested_transaction_restore_state_in_save @@ -175,11 +247,13 @@ class TransactionTest < ActiveRecord::TestCase raise "Make the transaction rollback" end - assert_raises(RuntimeError) do - Topic.transaction { topic.save } + assert_not_called(topic, :rolledback!) do + assert_raises(RuntimeError) do + Topic.transaction { topic.save } + end end - assert topic.new_record?, "#{topic.inspect} should be new record" + assert_predicate topic, :new_record?, "#{topic.inspect} should be new record" end def test_transaction_state_is_cleared_when_record_is_persisted @@ -587,7 +661,7 @@ class TransactionTest < ActiveRecord::TestCase def test_rollback_when_saving_a_frozen_record topic = Topic.new(title: "test") topic.freeze - e = assert_raise(frozen_error_class) { topic.save } + e = assert_raise(FrozenError) { topic.save } # Not good enough, but we can't do much # about it since there is no specific error # for frozen objects. @@ -884,17 +958,6 @@ class TransactionTest < ActiveRecord::TestCase assert_predicate transaction.state, :committed? end - def test_set_state_method_is_deprecated - connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction - - transaction.commit - - assert_deprecated do - transaction.state.set_state(:rolledback) - end - end - def test_mark_transaction_state_as_committed connection = Topic.connection transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction @@ -1014,7 +1077,6 @@ class TransactionTest < ActiveRecord::TestCase end private - %w(validation save destroy).each do |filter| define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do |topic| meta = class << topic; self; end diff --git a/activerecord/test/cases/type/time_test.rb b/activerecord/test/cases/type/time_test.rb new file mode 100644 index 0000000000..1a2c47479f --- /dev/null +++ b/activerecord/test/cases/type/time_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +module ActiveRecord + module Type + class TimeTest < ActiveRecord::TestCase + def test_default_year_is_correct + expected_time = ::Time.utc(2000, 1, 1, 10, 30, 0) + topic = Topic.new(bonus_time: { 4 => 10, 5 => 30 }) + + assert_equal expected_time, topic.bonus_time + + topic.save! + topic.reload + + assert_equal expected_time, topic.bonus_time + end + end + end +end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index 9eefc32745..49746996bc 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -11,6 +11,12 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase def setup @underlying = ActiveRecord::Base.connection @specification = ActiveRecord::Base.remove_connection + + # Clear out connection info from other pids (like a fork parent) too + pool_map = ActiveRecord::Base.connection_handler.instance_variable_get(:@owner_to_pool) + (pool_map.keys - [Process.pid]).each do |other_pid| + pool_map.delete(other_pid) + end end teardown do @@ -29,6 +35,14 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase end end + def test_error_message_when_connection_not_established + error = assert_raise(ActiveRecord::ConnectionNotEstablished) do + TestRecord.find(1) + end + + assert_equal "No connection pool with 'primary' found.", error.message + end + def test_underlying_adapter_no_longer_active assert_not @underlying.active?, "Removed adapter should no longer be active" end diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb index d5d8f2a09a..87edb163f2 100644 --- a/activerecord/test/cases/unsafe_raw_sql_test.rb +++ b/activerecord/test/cases/unsafe_raw_sql_test.rb @@ -77,7 +77,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase assert_equal ids_expected, ids_disabled end - test "order: allows table and column name" do + test "order: allows table and column names" do ids_expected = Post.order(Arel.sql("title")).pluck(:id) ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title").pluck(:id) } @@ -87,6 +87,17 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase assert_equal ids_expected, ids_disabled end + test "order: allows quoted table and column names" do + ids_expected = Post.order(Arel.sql("title")).pluck(:id) + + quoted_title = Post.connection.quote_table_name("posts.title") + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(quoted_title).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(quoted_title).pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + test "order: allows column name and direction in string" do ids_expected = Post.order(Arel.sql("title desc")).pluck(:id) @@ -116,10 +127,10 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase ["asc", "desc", ""].each do |direction| %w(first last).each do |position| - ids_expected = Post.order(Arel.sql("type #{direction} nulls #{position}")).pluck(:id) + ids_expected = Post.order(Arel.sql("type::text #{direction} nulls #{position}")).pluck(:id) - ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type #{direction} nulls #{position}").pluck(:id) } - ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type #{direction} nulls #{position}").pluck(:id) } + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type::text #{direction} nulls #{position}").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type::text #{direction} nulls #{position}").pluck(:id) } assert_equal ids_expected, ids_depr assert_equal ids_expected, ids_disabled @@ -130,7 +141,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "order: disallows invalid column name" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.order("len(title) asc").pluck(:id) + Post.order("REPLACE(title, 'misc', 'zzzz') asc").pluck(:id) end end end @@ -146,7 +157,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "order: disallows invalid column with direction" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.order("len(title)" => :asc).pluck(:id) + Post.order("REPLACE(title, 'misc', 'zzzz')" => :asc).pluck(:id) end end end @@ -179,7 +190,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "order: disallows invalid Array arguments" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.order(["author_id", "length(title)"]).pluck(:id) + Post.order(["author_id", "REPLACE(title, 'misc', 'zzzz')"]).pluck(:id) end end end @@ -187,8 +198,8 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "order: allows valid Array arguments" do ids_expected = Post.order(Arel.sql("author_id, length(title)")).pluck(:id) - ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) } - ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) } + ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", "length(title)"]).pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", "length(title)"]).pluck(:id) } assert_equal ids_expected, ids_depr assert_equal ids_expected, ids_disabled @@ -197,7 +208,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "order: logs deprecation warning for unrecognized column" do with_unsafe_raw_sql_deprecated do assert_deprecated(/Dangerous query method/) do - Post.order("length(title)") + Post.order("REPLACE(title, 'misc', 'zzzz')") end end end @@ -212,6 +223,16 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase assert_equal titles_expected, titles_disabled end + test "pluck: allows string column name with function and alias" do + titles_expected = Post.pluck(Arel.sql("UPPER(title)")) + + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("UPPER(title) AS title") } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("UPPER(title) AS title") } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + test "pluck: allows symbol column name" do titles_expected = Post.pluck(Arel.sql("title")) @@ -262,10 +283,21 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase assert_equal titles_expected, titles_disabled end + test "pluck: allows quoted table and column names" do + titles_expected = Post.pluck(Arel.sql("title")) + + quoted_title = Post.connection.quote_table_name("posts.title") + titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck(quoted_title) } + titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck(quoted_title) } + + assert_equal titles_expected, titles_depr + assert_equal titles_expected, titles_disabled + end + test "pluck: disallows invalid column name" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.pluck("length(title)") + Post.pluck("REPLACE(title, 'misc', 'zzzz')") end end end @@ -273,7 +305,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "pluck: disallows invalid column name amongst valid names" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.pluck(:title, "length(title)") + Post.pluck(:title, "REPLACE(title, 'misc', 'zzzz')") end end end @@ -281,7 +313,7 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "pluck: disallows invalid column names with includes" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.includes(:comments).pluck(:title, "length(title)") + Post.includes(:comments).pluck(:title, "REPLACE(title, 'misc', 'zzzz')") end end end @@ -296,24 +328,25 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase test "pluck: logs deprecation warning" do with_unsafe_raw_sql_deprecated do assert_deprecated(/Dangerous query method/) do - Post.includes(:comments).pluck(:title, "length(title)") + Post.includes(:comments).pluck(:title, "REPLACE(title, 'misc', 'zzzz')") end end end - def with_unsafe_raw_sql_disabled(&blk) - with_config(:disabled, &blk) - end + private + def with_unsafe_raw_sql_disabled(&block) + with_config(:disabled, &block) + end - def with_unsafe_raw_sql_deprecated(&blk) - with_config(:deprecated, &blk) - end + def with_unsafe_raw_sql_deprecated(&block) + with_config(:deprecated, &block) + end - def with_config(new_value, &blk) - old_value = ActiveRecord::Base.allow_unsafe_raw_sql - ActiveRecord::Base.allow_unsafe_raw_sql = new_value - blk.call - ensure - ActiveRecord::Base.allow_unsafe_raw_sql = old_value - end + def with_config(new_value, &block) + old_value = ActiveRecord::Base.allow_unsafe_raw_sql + ActiveRecord::Base.allow_unsafe_raw_sql = new_value + yield + ensure + ActiveRecord::Base.allow_unsafe_raw_sql = old_value + end end diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index 8235a54d8a..1982734f02 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -61,7 +61,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase def test_validates_absence_of_virtual_attribute_on_model repair_validations(Interest) do - Interest.send(:attr_accessor, :token) + Interest.attr_accessor(:token) Interest.validates_absence_of(:token) interest = Interest.create!(topic: "Thought Leadering") diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index 703c24b340..993c201f03 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -4,16 +4,20 @@ require "cases/helper" require "models/topic" class I18nGenerateMessageValidationTest < ActiveRecord::TestCase + class Backend < I18n::Backend::Simple + include I18n::Backend::Fallbacks + end + def setup Topic.clear_validators! @topic = Topic.new - I18n.backend = I18n::Backend::Simple.new + I18n.backend = Backend.new end def reset_i18n_load_path @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend I18n.load_path.clear - I18n.backend = I18n::Backend::Simple.new + I18n.backend = Backend.new yield ensure I18n.load_path.replace @old_load_path @@ -83,4 +87,16 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title") end end + + test "activerecord attributes scope falls back to parent locale before it falls back to the :errors namespace" do + reset_i18n_load_path do + I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { attributes: { title: { taken: "custom en message" } } } } } } + I18n.backend.store_translations "en-US", errors: { messages: { taken: "generic en-US fallback" } } + + I18n.with_locale "en-US" do + assert_equal "custom en message", @topic.errors.generate_message(:title, :taken, value: "title") + assert_equal "generic en-US fallback", @topic.errors.generate_message(:heading, :taken, value: "heading") + end + end + end end diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index b7c52ea18c..4dd8a4a82b 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -40,19 +40,20 @@ class I18nValidationTest < ActiveRecord::TestCase COMMON_CASES = [ # [ case, validation_options, generate_message_options] [ "given no options", {}, {}], - [ "given custom message", { message: "custom" }, { message: "custom" }], - [ "given if condition", { if: lambda { true } }, {}], - [ "given unless condition", { unless: lambda { false } }, {}], - [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }], - [ "given on condition", { on: [:create, :update] }, {}] + [ "given custom message", { message: "custom" }, { message: "custom" }], + [ "given if condition", { if: lambda { true } }, {}], + [ "given unless condition", { unless: lambda { false } }, {}], + [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }], + [ "given on condition", { on: [:create, :update] }, {}] ] COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_uniqueness_of on generated message #{name}" do Topic.validates_uniqueness_of :title, validation_options @topic.title = unique_topic.title - assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(value: "unique!")]) do + assert_called_with(ActiveModel::Error, :generate_message, [:title, :taken, @topic, generate_message_options.merge(value: "unique!")]) do @topic.valid? + @topic.errors.messages end end end @@ -60,8 +61,9 @@ class I18nValidationTest < ActiveRecord::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_associated on generated message #{name}" do Topic.validates_associated :replies, validation_options - assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(value: replied_topic.replies)]) do + assert_called_with(ActiveModel::Error, :generate_message, [:replies, :invalid, replied_topic, generate_message_options.merge(value: replied_topic.replies)]) do replied_topic.save + replied_topic.errors.messages end end end diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 1fbcdc271b..a7cb718043 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -64,7 +64,7 @@ class LengthValidationTest < ActiveRecord::TestCase def test_validates_length_of_virtual_attribute_on_model repair_validations(Pet) do - Pet.send(:attr_accessor, :nickname) + Pet.attr_accessor(:nickname) Pet.validates_length_of(:name, minimum: 1) Pet.validates_length_of(:nickname, minimum: 1) diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 63c3f67da2..4b9cbe9098 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -69,7 +69,7 @@ class PresenceValidationTest < ActiveRecord::TestCase def test_validates_presence_of_virtual_attribute_on_model repair_validations(Interest) do - Interest.send(:attr_accessor, :abbreviation) + Interest.attr_accessor(:abbreviation) Interest.validates_presence_of(:topic) Interest.validates_presence_of(:abbreviation) diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 8f6f47e5fb..76163e3093 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -314,6 +314,51 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t3.save, "Should save t3 as unique" end + if current_adapter?(:Mysql2Adapter) + def test_deprecate_validate_uniqueness_mismatched_collation + Topic.validates_uniqueness_of(:author_email_address) + + topic1 = Topic.new(author_email_address: "david@loudthinking.com") + topic2 = Topic.new(author_email_address: "David@loudthinking.com") + + assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count + + assert_deprecated do + assert_not topic1.valid? + assert_not topic1.save + assert topic2.valid? + assert topic2.save + end + + assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count + assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count + end + end + + def test_validate_case_sensitive_uniqueness_by_default + Topic.validates_uniqueness_of(:author_email_address) + + topic1 = Topic.new(author_email_address: "david@loudthinking.com") + topic2 = Topic.new(author_email_address: "David@loudthinking.com") + + assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count + + ActiveSupport::Deprecation.silence do + assert_not topic1.valid? + assert_not topic1.save + assert topic2.valid? + assert topic2.save + end + + if current_adapter?(:Mysql2Adapter) + assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count + assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count + else + assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count + assert_equal 1, Topic.where(author_email_address: "David@loudthinking.com").count + end + end + def test_validate_case_sensitive_uniqueness Topic.validates_uniqueness_of(:title, case_sensitive: true, allow_nil: true) @@ -510,7 +555,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase abc.save! end assert_match(/\AUnknown primary key for table dashboards in model/, e.message) - assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) + assert_match(/Cannot validate uniqueness for persisted record without primary key.\z/, e.message) end def test_validate_uniqueness_ignores_itself_when_primary_key_changed diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 66763c727f..4f98a6b7fc 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -144,9 +144,18 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal "100,000", d.salary_before_type_cast end + def test_validates_acceptance_of_with_undefined_attribute_methods + klass = Class.new(Topic) + klass.validates_acceptance_of(:approved) + topic = klass.new(approved: true) + klass.undefine_attribute_methods + assert topic.approved + end + def test_validates_acceptance_of_as_database_column - Topic.validates_acceptance_of(:approved) - topic = Topic.create("approved" => true) + klass = Class.new(Topic) + klass.validates_acceptance_of(:approved) + topic = klass.create("approved" => true) assert topic["approved"] end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 7e2d66c62a..36b9df7ba5 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -20,7 +20,7 @@ module ViewBehavior def setup super @connection = ActiveRecord::Base.connection - create_view "ebooks'", <<-SQL + create_view "ebooks'", <<~SQL SELECT id, name, status FROM books WHERE format = 'ebook' SQL end @@ -106,7 +106,7 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE VIEW paperbacks AS SELECT name, status FROM books WHERE format = 'paperback' SQL @@ -156,8 +156,7 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :SQLServerAdapter) || - current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.postgresql_version >= 90300 + if current_adapter?(:Mysql2Adapter, :SQLServerAdapter, :PostgreSQLAdapter) class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -169,7 +168,7 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE VIEW printed_books AS SELECT id, name, status, format FROM books WHERE format = 'paperback' SQL @@ -207,8 +206,7 @@ if ActiveRecord::Base.connection.supports_views? end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)` end # end of `if ActiveRecord::Base.connection.supports_views?` -if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && - ActiveRecord::Base.connection.supports_materialized_views? +if ActiveRecord::Base.connection.supports_materialized_views? class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase include ViewBehavior diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 60ebdce178..7003afa33a 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -130,7 +130,6 @@ class YamlSerializationTest < ActiveRecord::TestCase end private - def yaml_fixture(file_name) path = File.expand_path( "../support/yaml_compatibility_fixtures/#{file_name}.yml", diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 18347cd07d..f5e3ac3c19 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -1,7 +1,5 @@ default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> -with_manual_interventions: false - connections: jdbcderby: arunit: activerecord_unittest @@ -56,10 +54,16 @@ connections: username: rails encoding: utf8mb4 collation: utf8mb4_unicode_ci +<% if ENV['MYSQL_HOST'] %> + host: <%= ENV['MYSQL_HOST'] %> +<% end %> arunit2: username: rails encoding: utf8mb4 collation: utf8mb4_general_ci +<% if ENV['MYSQL_HOST'] %> + host: <%= ENV['MYSQL_HOST'] %> +<% end %> oracle: arunit: diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8b5a2fa0c8..da7e4139b1 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -116,6 +116,7 @@ class Author < ActiveRecord::Base has_many :tags_with_primary_key, through: :posts has_many :books + has_many :published_books, class_name: "PublishedBook" has_many :unpublished_books, -> { where(status: [:proposed, :written]) }, class_name: "Book" has_many :subscriptions, through: :books has_many :subscribers, -> { order("subscribers.nick") }, through: :subscriptions @@ -153,6 +154,7 @@ class Author < ActiveRecord::Base has_many :comments_on_posts_with_default_include, through: :posts_with_default_include, source: :comments has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post" + has_many :posts_mentioning_author, ->(record = nil) { where("posts.body LIKE ?", "%#{record&.name&.downcase}%") }, class_name: "Post" has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do def extension_method; end @@ -220,3 +222,12 @@ class AuthorFavorite < ActiveRecord::Base belongs_to :author belongs_to :favorite_author, class_name: "Author" end + +class AuthorFavoriteWithScope < ActiveRecord::Base + self.table_name = "author_favorites" + + default_scope { order(id: :asc) } + + belongs_to :author + belongs_to :favorite_author, class_name: "Author" +end diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb index be08636ac6..20af7c6122 100644 --- a/activerecord/test/models/bird.rb +++ b/activerecord/test/models/bird.rb @@ -6,9 +6,19 @@ class Bird < ActiveRecord::Base accepts_nested_attributes_for :pirate + before_save do + # force materialize_transactions + self.class.connection.materialize_transactions + end + attr_accessor :cancel_save_from_callback before_save :cancel_save_callback_method, if: :cancel_save_from_callback def cancel_save_callback_method throw(:abort) end + + attr_accessor :total_count, :enable_count + after_initialize do + self.total_count = Bird.count if enable_count + end end diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index afdda1a81e..43b82e6047 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -24,3 +24,9 @@ class Book < ActiveRecord::Base "do publish work..." end end + +class PublishedBook < ActiveRecord::Base + self.table_name = "books" + + validates_uniqueness_of :isbn +end diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 2ccc00bed9..8c86879dc6 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -26,6 +26,7 @@ class Category < ActiveRecord::Base has_many :categorizations has_many :special_categorizations has_many :post_comments, through: :posts, source: :comments + has_many :ordered_post_comments, -> { order(id: :desc) }, through: :posts, source: :comments has_many :authors, through: :categorizations has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 2006e05fcf..890e427616 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Club < ActiveRecord::Base - has_one :membership + has_one :membership, touch: true has_many :memberships, inverse_of: false has_many :members, through: :memberships has_one :sponsor @@ -10,10 +10,11 @@ class Club < ActiveRecord::Base has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member - scope :general, -> { left_joins(:category).where(categories: { name: "General" }) } + scope :general, -> { left_joins(:category).where(categories: { name: "General" }).unscope(:limit) } - private + accepts_nested_attributes_for :membership + private def private_method "I'm sorry sir, this is a *private* club, not a *pirate* club" end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 838f515aad..339b5c8ca8 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -13,6 +13,8 @@ class Company < AbstractCompany has_many :contracts has_many :developers, through: :contracts + attribute :metadata, :json + scope :of_first_firm, lambda { joins(account: :firm). where("firms.id" => 1) @@ -23,7 +25,6 @@ class Company < AbstractCompany end private - def private_method "I am Jack's innermost fears and aspirations" end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 52b7e06a63..320b26b950 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -91,7 +91,6 @@ module MyApplication validate :check_empty_credit_limit private - def check_empty_credit_limit errors.add("credit_card", :blank) if credit_card.blank? end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 6e02ff199b..d5f6f00691 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -10,14 +10,14 @@ module ContactFakeColumns table_name => "id" } - column :id, :integer - column :name, :string - column :age, :integer - column :avatar, :binary - column :created_at, :datetime - column :awesome, :boolean - column :preferences, :string - column :alternative_id, :integer + column :id, "integer" + column :name, "string" + column :age, "integer" + column :avatar, "binary" + column :created_at, "datetime" + column :awesome, "boolean" + column :preferences, "string" + column :alternative_id, "integer" serialize :preferences @@ -37,7 +37,7 @@ end class ContactSti < ActiveRecord::Base extend ContactFakeColumns - column :type, :string + column :type, "string" def type; "ContactSti" end end diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index 3f663375c4..89719775c4 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -5,7 +5,9 @@ class Contract < ActiveRecord::Base belongs_to :developer, primary_key: :id belongs_to :firm, foreign_key: "company_id" - before_save :hi + attribute :metadata, :json + + before_save :hi, :update_metadata after_save :bye attr_accessor :hi_count, :bye_count @@ -19,6 +21,10 @@ class Contract < ActiveRecord::Base @bye_count ||= 0 @bye_count += 1 end + + def update_metadata + self.metadata = { company_id: company_id, developer_id: developer_id } + end end class NewContract < Contract diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 8881c69368..92d01ba338 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -2,13 +2,13 @@ require "ostruct" -module DeveloperProjectsAssociationExtension2 - def find_least_recent - order("id ASC").first +class Developer < ActiveRecord::Base + module ProjectsAssociationExtension2 + def find_least_recent + order("id ASC").first + end end -end -class Developer < ActiveRecord::Base self.ignored_columns = %w(first_name last_name) has_and_belongs_to_many :projects do @@ -24,19 +24,19 @@ class Developer < ActiveRecord::Base has_and_belongs_to_many :shared_computers, class_name: "Computer" has_and_belongs_to_many :projects_extended_by_name, - -> { extending(DeveloperProjectsAssociationExtension) }, + -> { extending(ProjectsAssociationExtension) }, class_name: "Project", join_table: "developers_projects", association_foreign_key: "project_id" has_and_belongs_to_many :projects_extended_by_name_twice, - -> { extending(DeveloperProjectsAssociationExtension, DeveloperProjectsAssociationExtension2) }, + -> { extending(ProjectsAssociationExtension, ProjectsAssociationExtension2) }, class_name: "Project", join_table: "developers_projects", association_foreign_key: "project_id" has_and_belongs_to_many :projects_extended_by_name_and_block, - -> { extending(DeveloperProjectsAssociationExtension) }, + -> { extending(ProjectsAssociationExtension) }, class_name: "Project", join_table: "developers_projects", association_foreign_key: "project_id" do @@ -207,6 +207,7 @@ end class MultiplePoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = "developers" + default_scope { } default_scope -> { where(name: "Jamis") } default_scope -> { where(salary: 50000) } end @@ -279,3 +280,17 @@ class DeveloperWithIncorrectlyOrderedHasManyThrough < ActiveRecord::Base has_many :companies, through: :contracts has_many :contracts, foreign_key: :developer_id end + +class DeveloperName < ActiveRecord::Type::String + def deserialize(value) + "Developer: #{value}" + end +end + +class AttributedDeveloper < ActiveRecord::Base + self.table_name = "developers" + + attribute :name, DeveloperName.new + + self.ignored_columns += ["name"] +end diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb index eb6701b84e..8258408f35 100644 --- a/activerecord/test/models/drink_designer.rb +++ b/activerecord/test/models/drink_designer.rb @@ -4,5 +4,11 @@ class DrinkDesigner < ActiveRecord::Base has_one :chef, as: :employable end +class DrinkDesignerWithPolymorphicDependentNullifyChef < ActiveRecord::Base + self.table_name = "drink_designers" + + has_one :chef, as: :employable, dependent: :nullify +end + class MocktailDesigner < DrinkDesigner end diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index e900fd40fb..45ccc442ba 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -6,7 +6,7 @@ class Face < ActiveRecord::Base belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly` belongs_to :poly_man_without_inverse, polymorphic: true - # These is a "broken" inverse_of for the purposes of testing + # These are "broken" inverse_of associations for the purposes of testing belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 5cba1e440e..0dfd29e45e 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -62,6 +62,11 @@ class PersonWithDependentNullifyJobs < ActiveRecord::Base has_many :jobs, source: :job, through: :references, dependent: :nullify end +class PersonWithPolymorphicDependentNullifyComments < ActiveRecord::Base + self.table_name = "people" + has_many :comments, as: :author, dependent: :nullify +end + class LoosePerson < ActiveRecord::Base self.table_name = "people" self.abstract_class = true @@ -96,7 +101,6 @@ class RichPerson < ActiveRecord::Base before_validation :run_before_validation private - def run_before_create self.first_name = first_name.to_s + "run_before_create" end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index fd5083e597..8733398697 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -98,3 +98,19 @@ class FamousPirate < ActiveRecord::Base has_many :famous_ships validates_presence_of :catchphrase, on: :conference end + +class SpacePirate < ActiveRecord::Base + self.table_name = "pirates" + + belongs_to :parrot + belongs_to :parrot_with_annotation, -> { annotate("that tells jokes") }, class_name: :Parrot, foreign_key: :parrot_id + has_and_belongs_to_many :parrots, foreign_key: :pirate_id + has_and_belongs_to_many :parrots_with_annotation, -> { annotate("that are very colorful") }, class_name: :Parrot, foreign_key: :pirate_id + has_one :ship, foreign_key: :pirate_id + has_one :ship_with_annotation, -> { annotate("that is a rocket") }, class_name: :Ship, foreign_key: :pirate_id + has_many :birds, foreign_key: :pirate_id + has_many :birds_with_annotation, -> { annotate("that are also parrots") }, class_name: :Bird, foreign_key: :pirate_id + has_many :treasures, as: :looter + has_many :treasure_estimates, through: :treasures, source: :price_estimates + has_many :treasure_estimates_with_annotation, -> { annotate("yarrr") }, through: :treasures, source: :price_estimates +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 710a75ad30..50c0dddcf2 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -11,6 +11,10 @@ class Post < ActiveRecord::Base def author "lifo" end + + def greeting + super + " :)" + end end module NamedExtension2 @@ -31,6 +35,7 @@ class Post < ActiveRecord::Base belongs_to :author_with_posts, -> { includes(:posts) }, class_name: "Author", foreign_key: :author_id belongs_to :author_with_address, -> { includes(:author_address) }, class_name: "Author", foreign_key: :author_id + belongs_to :author_with_select, -> { select(:id) }, class_name: "Author", foreign_key: :author_id def first_comment super.body @@ -38,6 +43,7 @@ class Post < ActiveRecord::Base has_one :first_comment, -> { order("id ASC") }, class_name: "Comment" has_one :last_comment, -> { order("id desc") }, class_name: "Comment" + scope :no_comments, -> { left_joins(:comments).where(comments: { id: nil }) } scope :with_special_comments, -> { joins(:comments).where(comments: { type: "SpecialComment" }) } scope :with_very_special_comments, -> { joins(:comments).where(comments: { type: "VerySpecialComment" }) } scope :with_post, ->(post_id) { joins(:comments).where(comments: { post_id: post_id }) } @@ -77,6 +83,7 @@ class Post < ActiveRecord::Base has_many :comments_with_extend_2, extend: [NamedExtension, NamedExtension2], class_name: "Comment", foreign_key: "post_id" has_many :author_favorites, through: :author + has_many :author_favorites_with_scope, through: :author, class_name: "AuthorFavoriteWithScope", source: "author_favorites" has_many :author_categorizations, through: :author, source: :categorizations has_many :author_addresses, through: :author has_many :author_address_extra_with_address, @@ -201,6 +208,10 @@ end class SubAbstractStiPost < AbstractStiPost; end +class NullPost < Post + default_scope { none } +end + class FirstPost < ActiveRecord::Base self.inheritance_column = :disabled self.table_name = "posts" @@ -210,6 +221,12 @@ class FirstPost < ActiveRecord::Base has_one :comment, foreign_key: :post_id end +class PostWithDefaultSelect < ActiveRecord::Base + self.table_name = "posts" + + default_scope { select(:author_id) } +end + class TaggedPost < Post has_many :taggings, -> { rewhere(taggable_type: "TaggedPost") }, as: :taggable has_many :tags, through: :taggings @@ -254,6 +271,7 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base self.table_name = "posts" default_scope { where(id: [1, 5, 6]) } scope :unscoped_all, -> { unscoped { all } } + scope :authorless, -> { unscoped { where(author_id: 0) } } end class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base @@ -306,8 +324,8 @@ class FakeKlass "posts" end - def attribute_alias?(name) - false + def attribute_aliases + {} end def sanitize_sql(sql) @@ -326,6 +344,10 @@ class FakeKlass # noop end + def columns_hash + { "name" => nil } + end + def arel_table Post.arel_table end diff --git a/activerecord/test/models/rating.rb b/activerecord/test/models/rating.rb index cf06bc6931..2a18ea45ac 100644 --- a/activerecord/test/models/rating.rb +++ b/activerecord/test/models/rating.rb @@ -3,4 +3,6 @@ class Rating < ActiveRecord::Base belongs_to :comment has_many :taggings, as: :taggable + has_many :taggings_without_tag, -> { left_joins(:tag).where("tags.id": nil) }, as: :taggable, class_name: "Tagging" + has_many :taggings_with_no_tag, -> { joins("LEFT OUTER JOIN tags ON tags.id = taggings.tag_id").where("tags.id": nil) }, as: :taggable, class_name: "Tagging" end diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 2a7a1e3b77..82185040d6 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -4,6 +4,7 @@ class Reference < ActiveRecord::Base belongs_to :person belongs_to :job + has_many :ideal_jobs, class_name: "Job", foreign_key: :ideal_reference_id has_many :agents_posts_authors, through: :person class << self; attr_accessor :make_comments; end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 0807bcf875..f6ab9c8a8f 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -7,6 +7,8 @@ class Reply < Topic belongs_to :topic_with_primary_key, class_name: "Topic", primary_key: "title", foreign_key: "parent_title", counter_cache: "replies_count", touch: true has_many :replies, class_name: "SillyReply", dependent: :destroy, foreign_key: "parent_id" has_many :silly_unique_replies, dependent: :destroy, foreign_key: "parent_id" + + scope :ordered, -> { Reply.order(:id) } end class SillyReply < Topic @@ -32,29 +34,29 @@ class WrongReply < Reply validate :check_author_name_is_secret, on: :special_case def check_empty_title - errors[:title] << "Empty" unless attribute_present?("title") + errors.add(:title, "Empty") unless attribute_present?("title") end def errors_on_empty_content - errors[:content] << "Empty" unless attribute_present?("content") + errors.add(:content, "Empty") unless attribute_present?("content") end def check_content_mismatch if attribute_present?("title") && attribute_present?("content") && content == "Mismatch" - errors[:title] << "is Content Mismatch" + errors.add(:title, "is Content Mismatch") end end def title_is_wrong_create - errors[:title] << "is Wrong Create" if attribute_present?("title") && title == "Wrong Create" + errors.add(:title, "is Wrong Create") if attribute_present?("title") && title == "Wrong Create" end def check_wrong_update - errors[:title] << "is Wrong Update" if attribute_present?("title") && title == "Wrong Update" + errors.add(:title, "is Wrong Update") if attribute_present?("title") && title == "Wrong Update" end def check_author_name_is_secret - errors[:author_name] << "Invalid" unless author_name == "secret" + errors.add(:author_name, "Invalid") unless author_name == "secret" end end diff --git a/activerecord/test/models/section.rb b/activerecord/test/models/section.rb new file mode 100644 index 0000000000..f8b4cc7936 --- /dev/null +++ b/activerecord/test/models/section.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Section < ActiveRecord::Base + belongs_to :session, inverse_of: :sections, autosave: true + belongs_to :seminar, inverse_of: :sections, autosave: true +end diff --git a/activerecord/test/models/seminar.rb b/activerecord/test/models/seminar.rb new file mode 100644 index 0000000000..c18aa86433 --- /dev/null +++ b/activerecord/test/models/seminar.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Seminar < ActiveRecord::Base + has_many :sections, inverse_of: :seminar, autosave: true, dependent: :destroy + has_many :sessions, through: :sections +end diff --git a/activerecord/test/models/session.rb b/activerecord/test/models/session.rb new file mode 100644 index 0000000000..db66b5297e --- /dev/null +++ b/activerecord/test/models/session.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Session < ActiveRecord::Base + has_many :sections, inverse_of: :session, autosave: true, dependent: :destroy + has_many :seminars, through: :sections +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 7973219a79..6bab7a1eb9 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -27,7 +27,8 @@ class ShipWithoutNestedAttributes < ActiveRecord::Base has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id has_many :parts, class_name: "ShipPart", foreign_key: :ship_id - validates :name, presence: true + validates :name, presence: true, if: -> { true } + validates :name, presence: true, if: -> { true } end class Prisoner < ActiveRecord::Base diff --git a/activerecord/test/models/subscription.rb b/activerecord/test/models/subscription.rb index d1d5d21621..f87315fcd1 100644 --- a/activerecord/test/models/subscription.rb +++ b/activerecord/test/models/subscription.rb @@ -3,4 +3,6 @@ class Subscription < ActiveRecord::Base belongs_to :subscriber, counter_cache: :books_count belongs_to :book + + validates_presence_of :subscriber_id, :book_id end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 03430154db..7a864c728c 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -12,17 +12,9 @@ class Topic < ActiveRecord::Base scope :scope_with_lambda, lambda { all } - scope :by_private_lifo, -> { where(author_name: private_lifo) } scope :by_lifo, -> { where(author_name: "lifo") } scope :replied, -> { where "replies_count > 0" } - class << self - private - def private_lifo - "lifo" - end - end - scope "approved_as_string", -> { where(approved: true) } scope :anonymous_extension, -> { } do def one @@ -96,14 +88,17 @@ class Topic < ActiveRecord::Base write_attribute(:approved, val) end - private + def self.nested_scoping(scope) + scope.base + end + private def default_written_on self.written_on = Time.now unless attribute_present?("written_on") end def destroy_children - self.class.where("parent_id = #{id}").delete_all + self.class.delete_by(parent_id: id) end def set_email_address @@ -123,10 +118,6 @@ class Topic < ActiveRecord::Base end end -class ImportantTopic < Topic - serialize :important, Hash -end - class DefaultRejectedTopic < Topic default_scope -> { where(approved: false) } end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index ccca9a2d9b..911ac808c6 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -27,6 +27,7 @@ ActiveRecord::Schema.define do create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 + t.tinyblob :tiny_blob t.blob :normal_blob t.mediumblob :medium_blob @@ -36,6 +37,13 @@ ActiveRecord::Schema.define do t.mediumtext :medium_text t.longtext :long_text + t.binary :tiny_blob_2, size: :tiny + t.binary :medium_blob_2, size: :medium + t.binary :long_blob_2, size: :long + t.text :tiny_text_2, size: :tiny + t.text :medium_text_2, size: :medium + t.text :long_text_2, size: :long + t.index :var_binary end @@ -54,33 +62,21 @@ ActiveRecord::Schema.define do t.binary :binary_column, limit: 1 end - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS ten; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE ten() SQL SECURITY INVOKER -BEGIN - select 10; -END -SQL - - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS topics; -SQL + execute "DROP PROCEDURE IF EXISTS ten" - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER -BEGIN - select * from topics limit num; -END -SQL + execute <<~SQL + CREATE PROCEDURE ten() SQL SECURITY INVOKER + BEGIN + SELECT 10; + END + SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true + execute "DROP PROCEDURE IF EXISTS topics" - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE enum_tests ( - enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint') -) -SQL + execute <<~SQL + CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER + BEGIN + SELECT * FROM topics LIMIT num; + END + SQL end diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index bc1e45ca80..08c6e24555 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -7,23 +7,21 @@ ActiveRecord::Schema.define do execute "drop table defaults" rescue nil execute "drop sequence defaults_seq" rescue nil - execute <<-SQL -create table test_oracle_defaults ( - id integer not null primary key, - test_char char(1) default 'X' not null, - test_string varchar2(20) default 'hello' not null, - test_int integer default 3 not null -) + execute <<~SQL + create table test_oracle_defaults ( + id integer not null primary key, + test_char char(1) default 'X' not null, + test_string varchar2(20) default 'hello' not null, + test_int integer default 3 not null + ) SQL - execute <<-SQL -create sequence test_oracle_defaults_seq minvalue 10000 - SQL + execute "create sequence test_oracle_defaults_seq minvalue 10000" execute "create sequence companies_nonstd_seq minvalue 10000" - execute <<-SQL - CREATE TABLE defaults ( + execute <<~SQL + CREATE TABLE defaults ( id integer not null, modified_date date default sysdate, modified_date_function date default sysdate, @@ -34,7 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000 char1 varchar2(1) default 'Y', char2 varchar2(50) default 'a varchar field', char3 clob default 'a text field' - ) + ) SQL execute "create sequence defaults_seq minvalue 10000" end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7034c773d2..b6c0ae0de2 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -8,10 +8,18 @@ ActiveRecord::Schema.define do # # # ------------------------------------------------------------------- # + case_sensitive_options = + if current_adapter?(:Mysql2Adapter) + { collation: "utf8mb4_bin" } + else + {} + end + create_table :accounts, force: true do |t| t.references :firm, index: false t.string :firm_name t.integer :credit_limit + t.integer "a" * max_identifier_length end create_table :admin_accounts, force: true do |t| @@ -94,18 +102,23 @@ ActiveRecord::Schema.define do end create_table :books, id: :integer, force: true do |t| + default_zero = { default: 0 } t.references :author t.string :format t.column :name, :string - t.column :status, :integer, default: 0 - t.column :read_status, :integer, default: 0 + t.column :status, :integer, **default_zero + t.column :read_status, :integer, **default_zero t.column :nullable_status, :integer - t.column :language, :integer, default: 0 - t.column :author_visibility, :integer, default: 0 - t.column :illustrator_visibility, :integer, default: 0 - t.column :font_size, :integer, default: 0 - t.column :difficulty, :integer, default: 0 + t.column :language, :integer, **default_zero + t.column :author_visibility, :integer, **default_zero + t.column :illustrator_visibility, :integer, **default_zero + t.column :font_size, :integer, **default_zero + t.column :difficulty, :integer, **default_zero t.column :cover, :string, default: "hard" + t.string :isbn, **case_sensitive_options + t.datetime :published_on + t.index [:author_id, :name], unique: true + t.index :isbn, where: "published_on IS NOT NULL", unique: true end create_table :booleans, force: true do |t| @@ -247,6 +260,8 @@ ActiveRecord::Schema.define do create_table :contracts, force: true do |t| t.references :developer, index: false t.references :company, index: false + t.string :metadata + t.integer :count end create_table :customers, force: true do |t| @@ -264,7 +279,7 @@ ActiveRecord::Schema.define do end create_table :dashboards, force: true, id: false do |t| - t.string :dashboard_id + t.string :dashboard_id, **case_sensitive_options t.string :name end @@ -328,7 +343,7 @@ ActiveRecord::Schema.define do end create_table :essays, force: true do |t| - t.string :name + t.string :name, **case_sensitive_options t.string :writer_id t.string :writer_type t.string :category_id @@ -336,7 +351,7 @@ ActiveRecord::Schema.define do end create_table :events, force: true do |t| - t.string :title, limit: 5 + t.string :title, limit: 5, **case_sensitive_options end create_table :eyes, force: true do |t| @@ -378,7 +393,7 @@ ActiveRecord::Schema.define do end create_table :guids, force: true do |t| - t.column :key, :string + t.column :key, :string, **case_sensitive_options end create_table :guitars, force: true do |t| @@ -386,8 +401,8 @@ ActiveRecord::Schema.define do end create_table :inept_wizards, force: true do |t| - t.column :name, :string, null: false - t.column :city, :string, null: false + t.column :name, :string, null: false, **case_sensitive_options + t.column :city, :string, null: false, **case_sensitive_options t.column :type, :string end @@ -510,6 +525,8 @@ ActiveRecord::Schema.define do t.integer :club_id, :member_id t.boolean :favourite, default: false t.integer :type + t.datetime :created_at + t.datetime :updated_at end create_table :member_types, force: true do |t| @@ -682,11 +699,7 @@ ActiveRecord::Schema.define do create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer - if subsecond_precision_supported? - t.timestamps null: false, precision: 6 - else - t.timestamps null: false - end + t.timestamps end create_table :pets_treasures, force: true do |t| @@ -782,6 +795,24 @@ ActiveRecord::Schema.define do t.integer :lock_version, default: 0 end + disable_referential_integrity do + create_table :seminars, force: :cascade do |t| + t.string :name + end + + create_table :sessions, force: :cascade do |t| + t.date :start_date + t.date :end_date + t.string :name + end + + create_table :sections, force: :cascade do |t| + t.string :short_name + t.belongs_to :session, foreign_key: true + t.belongs_to :seminar, foreign_key: true + end + end + create_table :shape_expressions, force: true do |t| t.string :paint_type t.integer :paint_id @@ -878,8 +909,8 @@ ActiveRecord::Schema.define do end create_table :topics, force: true do |t| - t.string :title, limit: 250 - t.string :author_name + t.string :title, limit: 250, **case_sensitive_options + t.string :author_name, **case_sensitive_options t.string :author_email_address if subsecond_precision_supported? t.datetime :written_on, precision: 6 @@ -891,10 +922,10 @@ ActiveRecord::Schema.define do # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) - t.string :content, limit: 4000 + t.string :content, limit: 4000, **case_sensitive_options t.string :important, limit: 4000 else - t.text :content + t.text :content, **case_sensitive_options t.text :important end t.boolean :approved, default: true @@ -904,11 +935,7 @@ ActiveRecord::Schema.define do t.string :parent_title t.string :type t.string :group - if subsecond_precision_supported? - t.timestamps null: true, precision: 6 - else - t.timestamps null: true - end + t.timestamps null: true end create_table :toys, primary_key: :toy_id, force: true do |t| diff --git a/activerecord/test/support/config.rb b/activerecord/test/support/config.rb index bd6d5c339b..66ae57b382 100644 --- a/activerecord/test/support/config.rb +++ b/activerecord/test/support/config.rb @@ -12,35 +12,34 @@ module ARTest end private + def config_file + Pathname.new(ENV["ARCONFIG"] || TEST_ROOT + "/config.yml") + end - def config_file - Pathname.new(ENV["ARCONFIG"] || TEST_ROOT + "/config.yml") - end + def read_config + unless config_file.exist? + FileUtils.cp TEST_ROOT + "/config.example.yml", config_file + end - def read_config - unless config_file.exist? - FileUtils.cp TEST_ROOT + "/config.example.yml", config_file + erb = ERB.new(config_file.read) + expand_config(YAML.parse(erb.result(binding)).transform) end - erb = ERB.new(config_file.read) - expand_config(YAML.parse(erb.result(binding)).transform) - end - - def expand_config(config) - config["connections"].each do |adapter, connection| - dbs = [["arunit", "activerecord_unittest"], ["arunit2", "activerecord_unittest2"], - ["arunit_without_prepared_statements", "activerecord_unittest"]] - dbs.each do |name, dbname| - unless connection[name].is_a?(Hash) - connection[name] = { "database" => connection[name] } + def expand_config(config) + config["connections"].each do |adapter, connection| + dbs = [["arunit", "activerecord_unittest"], ["arunit2", "activerecord_unittest2"], + ["arunit_without_prepared_statements", "activerecord_unittest"]] + dbs.each do |name, dbname| + unless connection[name].is_a?(Hash) + connection[name] = { "database" => connection[name] } + end + + connection[name]["database"] ||= dbname + connection[name]["adapter"] ||= adapter end - - connection[name]["database"] ||= dbname - connection[name]["adapter"] ||= adapter end - end - config - end + config + end end end diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 2a4fa53460..367309dd85 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -21,6 +21,7 @@ module ARTest def self.connect puts "Using #{connection_name}" ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) + ActiveRecord::Base.connection_handlers = { ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler } ActiveRecord::Base.configurations = connection_config ActiveRecord::Base.establish_connection :arunit ARUnit2Model.establish_connection :arunit2 diff --git a/activerecord/test/support/stubs/strong_parameters.rb b/activerecord/test/support/stubs/strong_parameters.rb new file mode 100644 index 0000000000..da8f9892f9 --- /dev/null +++ b/activerecord/test/support/stubs/strong_parameters.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "active_support/core_ext/hash/indifferent_access" + +class ProtectedParams + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + + def initialize(parameters = {}) + @parameters = parameters.with_indifferent_access + @permitted = false + end + + def permitted? + @permitted + end + + def permit! + @permitted = true + self + end + + def [](key) + @parameters[key] + end + + def to_h + @parameters.to_h + end + alias to_unsafe_h to_h + + def stringify_keys + dup + end + + def dup + super.tap do |duplicate| + duplicate.instance_variable_set :@permitted, @permitted + end + end +end |